package grpcserver import ( "context" "errors" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" pb "git.wntrmute.dev/mc/metacrypt/gen/metacrypt/v2" "git.wntrmute.dev/mc/metacrypt/internal/auth" "git.wntrmute.dev/mc/metacrypt/internal/engine" "git.wntrmute.dev/mc/metacrypt/internal/engine/user" "git.wntrmute.dev/mc/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 }