Replace metacrypt's hand-rolled gRPC interceptor chain with the mcdsl grpcserver package, which provides TLS setup, logging, and method-map auth (public/auth-required/admin-required) out of the box. Metacrypt-specific interceptors are preserved as hooks: - sealInterceptor runs as a PreInterceptor (before logging/auth) - auditInterceptor runs as a PostInterceptor (after auth) The three legacy method maps (seal/auth/admin) are restructured into mcdsl's MethodMap (Public/AuthRequired/AdminRequired) plus a separate seal-required map for the PreInterceptor. Token context is now stored via mcdsl/auth.ContextWithTokenInfo instead of a package-local key. Bumps mcdsl from v1.0.0 to v1.0.1 (adds PreInterceptors/PostInterceptors to grpcserver.Options). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
9.3 KiB
Go
275 lines
9.3 KiB
Go
package grpcserver
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
pb "git.wntrmute.dev/kyle/metacrypt/gen/metacrypt/v2"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/auth"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/engine/user"
|
|
"git.wntrmute.dev/kyle/metacrypt/internal/policy"
|
|
)
|
|
|
|
type userServer struct {
|
|
pb.UnimplementedUserServiceServer
|
|
s *GRPCServer
|
|
}
|
|
|
|
func (us *userServer) callerInfo(ctx context.Context) *engine.CallerInfo {
|
|
ti := auth.TokenInfoFromContext(ctx)
|
|
if ti == nil {
|
|
return nil
|
|
}
|
|
return &engine.CallerInfo{
|
|
Username: ti.Username,
|
|
Roles: ti.Roles,
|
|
IsAdmin: ti.IsAdmin,
|
|
}
|
|
}
|
|
|
|
func (us *userServer) policyChecker(ctx context.Context) engine.PolicyChecker {
|
|
caller := us.callerInfo(ctx)
|
|
if caller == nil {
|
|
return nil
|
|
}
|
|
return func(resource, action string) (string, bool) {
|
|
pReq := &policy.Request{
|
|
Username: caller.Username,
|
|
Roles: caller.Roles,
|
|
Resource: resource,
|
|
Action: action,
|
|
}
|
|
effect, matched, err := us.s.policy.Match(ctx, pReq)
|
|
if err != nil {
|
|
return string(policy.EffectDeny), false
|
|
}
|
|
return string(effect), matched
|
|
}
|
|
}
|
|
|
|
func (us *userServer) handleRequest(ctx context.Context, mount, operation string, req *engine.Request) (*engine.Response, error) {
|
|
resp, err := us.s.engines.HandleRequest(ctx, mount, req)
|
|
if err != nil {
|
|
st := codes.Internal
|
|
switch {
|
|
case errors.Is(err, engine.ErrMountNotFound):
|
|
st = codes.NotFound
|
|
case errors.Is(err, user.ErrUserNotFound):
|
|
st = codes.NotFound
|
|
case errors.Is(err, user.ErrUserExists):
|
|
st = codes.AlreadyExists
|
|
case errors.Is(err, user.ErrUnauthorized):
|
|
st = codes.Unauthenticated
|
|
case errors.Is(err, user.ErrForbidden):
|
|
st = codes.PermissionDenied
|
|
case errors.Is(err, user.ErrTooMany):
|
|
st = codes.InvalidArgument
|
|
case errors.Is(err, user.ErrNoRecipients):
|
|
st = codes.InvalidArgument
|
|
case errors.Is(err, user.ErrInvalidEnvelope):
|
|
st = codes.InvalidArgument
|
|
case errors.Is(err, user.ErrRecipientNotFound):
|
|
st = codes.NotFound
|
|
case errors.Is(err, user.ErrDecryptionFailed):
|
|
st = codes.InvalidArgument
|
|
case strings.Contains(err.Error(), "forbidden"):
|
|
st = codes.PermissionDenied
|
|
case strings.Contains(err.Error(), "not found"):
|
|
st = codes.NotFound
|
|
}
|
|
us.s.logger.Error("grpc: user "+operation, "mount", mount, "error", err)
|
|
return nil, status.Error(st, err.Error())
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (us *userServer) Register(ctx context.Context, req *pb.UserRegisterRequest) (*pb.UserRegisterResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "register", &engine.Request{
|
|
Operation: "register",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
username, _ := resp.Data["username"].(string)
|
|
pubKey, _ := resp.Data["public_key"].(string)
|
|
algorithm, _ := resp.Data["algorithm"].(string)
|
|
us.s.logger.Info("audit: user registered", "mount", req.Mount, "username", username)
|
|
return &pb.UserRegisterResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
|
}
|
|
|
|
func (us *userServer) Provision(ctx context.Context, req *pb.UserProvisionRequest) (*pb.UserProvisionResponse, error) {
|
|
if req.Mount == "" || req.Username == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "provision", &engine.Request{
|
|
Operation: "provision",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
Data: map[string]interface{}{"username": req.Username},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
username, _ := resp.Data["username"].(string)
|
|
pubKey, _ := resp.Data["public_key"].(string)
|
|
algorithm, _ := resp.Data["algorithm"].(string)
|
|
us.s.logger.Info("audit: user provisioned", "mount", req.Mount, "username", username, "by", callerUsername(ctx))
|
|
return &pb.UserProvisionResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
|
}
|
|
|
|
func (us *userServer) GetPublicKey(ctx context.Context, req *pb.UserGetPublicKeyRequest) (*pb.UserGetPublicKeyResponse, error) {
|
|
if req.Mount == "" || req.Username == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "get-public-key", &engine.Request{
|
|
Operation: "get-public-key",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
Data: map[string]interface{}{"username": req.Username},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
username, _ := resp.Data["username"].(string)
|
|
pubKey, _ := resp.Data["public_key"].(string)
|
|
algorithm, _ := resp.Data["algorithm"].(string)
|
|
return &pb.UserGetPublicKeyResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
|
}
|
|
|
|
func (us *userServer) ListUsers(ctx context.Context, req *pb.UserListUsersRequest) (*pb.UserListUsersResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "list-users", &engine.Request{
|
|
Operation: "list-users",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
raw, _ := resp.Data["users"].([]interface{})
|
|
users := make([]string, 0, len(raw))
|
|
for _, v := range raw {
|
|
if s, ok := v.(string); ok {
|
|
users = append(users, s)
|
|
}
|
|
}
|
|
return &pb.UserListUsersResponse{Users: users}, nil
|
|
}
|
|
|
|
func (us *userServer) Encrypt(ctx context.Context, req *pb.UserEncryptRequest) (*pb.UserEncryptResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
if req.Plaintext == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "plaintext is required")
|
|
}
|
|
if len(req.Recipients) == 0 {
|
|
return nil, status.Error(codes.InvalidArgument, "recipients are required")
|
|
}
|
|
recipients := make([]interface{}, len(req.Recipients))
|
|
for i, r := range req.Recipients {
|
|
recipients[i] = r
|
|
}
|
|
data := map[string]interface{}{
|
|
"plaintext": req.Plaintext,
|
|
"recipients": recipients,
|
|
}
|
|
if req.Metadata != "" {
|
|
data["metadata"] = req.Metadata
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "encrypt", &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
CheckPolicy: us.policyChecker(ctx),
|
|
Data: data,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
envelope, _ := resp.Data["envelope"].(string)
|
|
us.s.logger.Info("audit: user encrypt", "mount", req.Mount, "recipients", req.Recipients, "username", callerUsername(ctx))
|
|
return &pb.UserEncryptResponse{Envelope: envelope}, nil
|
|
}
|
|
|
|
func (us *userServer) Decrypt(ctx context.Context, req *pb.UserDecryptRequest) (*pb.UserDecryptResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
if req.Envelope == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "envelope is required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "decrypt", &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
plaintext, _ := resp.Data["plaintext"].(string)
|
|
sender, _ := resp.Data["sender"].(string)
|
|
metadata, _ := resp.Data["metadata"].(string)
|
|
return &pb.UserDecryptResponse{Plaintext: plaintext, Sender: sender, Metadata: metadata}, nil
|
|
}
|
|
|
|
func (us *userServer) ReEncrypt(ctx context.Context, req *pb.UserReEncryptRequest) (*pb.UserReEncryptResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
if req.Envelope == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "envelope is required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "re-encrypt", &engine.Request{
|
|
Operation: "re-encrypt",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
Data: map[string]interface{}{"envelope": req.Envelope},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
envelope, _ := resp.Data["envelope"].(string)
|
|
return &pb.UserReEncryptResponse{Envelope: envelope}, nil
|
|
}
|
|
|
|
func (us *userServer) RotateKey(ctx context.Context, req *pb.UserRotateKeyRequest) (*pb.UserRotateKeyResponse, error) {
|
|
if req.Mount == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount is required")
|
|
}
|
|
resp, err := us.handleRequest(ctx, req.Mount, "rotate-key", &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
username, _ := resp.Data["username"].(string)
|
|
pubKey, _ := resp.Data["public_key"].(string)
|
|
algorithm, _ := resp.Data["algorithm"].(string)
|
|
us.s.logger.Info("audit: user key rotated", "mount", req.Mount, "username", username)
|
|
return &pb.UserRotateKeyResponse{Username: username, PublicKey: pubKey, Algorithm: algorithm}, nil
|
|
}
|
|
|
|
func (us *userServer) DeleteUser(ctx context.Context, req *pb.UserDeleteUserRequest) (*pb.UserDeleteUserResponse, error) {
|
|
if req.Mount == "" || req.Username == "" {
|
|
return nil, status.Error(codes.InvalidArgument, "mount and username are required")
|
|
}
|
|
_, err := us.handleRequest(ctx, req.Mount, "delete-user", &engine.Request{
|
|
Operation: "delete-user",
|
|
CallerInfo: us.callerInfo(ctx),
|
|
Data: map[string]interface{}{"username": req.Username},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
us.s.logger.Info("audit: user deleted", "mount", req.Mount, "username", req.Username, "by", callerUsername(ctx))
|
|
return &pb.UserDeleteUserResponse{}, nil
|
|
}
|