Merge branch 'worktree-agent-a98b5183'

# Conflicts:
#	cmd/metacrypt/server.go
#	internal/grpcserver/server.go
#	internal/server/routes.go
This commit is contained in:
2026-03-16 20:01:04 -07:00
10 changed files with 4031 additions and 47 deletions

View File

@@ -82,6 +82,7 @@ func (s *GRPCServer) Start() error {
pb.RegisterCAServiceServer(s.srv, &caServer{s: s})
pb.RegisterPolicyServiceServer(s.srv, &policyServer{s: s})
pb.RegisterBarrierServiceServer(s.srv, &barrierServer{s: s})
pb.RegisterUserServiceServer(s.srv, &userServer{s: s})
pb.RegisterACMEServiceServer(s.srv, &acmeServer{s: s})
pb.RegisterSSHCAServiceServer(s.srv, &sshcaServer{s: s})
pb.RegisterTransitServiceServer(s.srv, &transitServer{s: s})
@@ -136,6 +137,15 @@ func sealRequiredMethods() map[string]bool {
"/metacrypt.v2.PolicyService/ListPolicies": true,
"/metacrypt.v2.PolicyService/GetPolicy": true,
"/metacrypt.v2.PolicyService/DeletePolicy": true,
"/metacrypt.v2.UserService/Register": true,
"/metacrypt.v2.UserService/Provision": true,
"/metacrypt.v2.UserService/GetPublicKey": true,
"/metacrypt.v2.UserService/ListUsers": true,
"/metacrypt.v2.UserService/Encrypt": true,
"/metacrypt.v2.UserService/Decrypt": true,
"/metacrypt.v2.UserService/ReEncrypt": true,
"/metacrypt.v2.UserService/RotateKey": true,
"/metacrypt.v2.UserService/DeleteUser": true,
"/metacrypt.v2.ACMEService/CreateEAB": true,
"/metacrypt.v2.ACMEService/SetConfig": true,
"/metacrypt.v2.ACMEService/ListAccounts": true,
@@ -202,6 +212,15 @@ func authRequiredMethods() map[string]bool {
"/metacrypt.v2.PolicyService/ListPolicies": true,
"/metacrypt.v2.PolicyService/GetPolicy": true,
"/metacrypt.v2.PolicyService/DeletePolicy": true,
"/metacrypt.v2.UserService/Register": true,
"/metacrypt.v2.UserService/Provision": true,
"/metacrypt.v2.UserService/GetPublicKey": true,
"/metacrypt.v2.UserService/ListUsers": true,
"/metacrypt.v2.UserService/Encrypt": true,
"/metacrypt.v2.UserService/Decrypt": true,
"/metacrypt.v2.UserService/ReEncrypt": true,
"/metacrypt.v2.UserService/RotateKey": true,
"/metacrypt.v2.UserService/DeleteUser": true,
"/metacrypt.v2.ACMEService/CreateEAB": true,
"/metacrypt.v2.ACMEService/SetConfig": true,
"/metacrypt.v2.ACMEService/ListAccounts": true,
@@ -258,6 +277,9 @@ func adminRequiredMethods() map[string]bool {
"/metacrypt.v2.PolicyService/ListPolicies": true,
"/metacrypt.v2.PolicyService/GetPolicy": true,
"/metacrypt.v2.PolicyService/DeletePolicy": true,
// User.
"/metacrypt.v2.UserService/Provision": true,
"/metacrypt.v2.UserService/DeleteUser": true,
"/metacrypt.v2.ACMEService/SetConfig": true,
"/metacrypt.v2.ACMEService/ListAccounts": true,
"/metacrypt.v2.ACMEService/ListOrders": true,

273
internal/grpcserver/user.go Normal file
View File

@@ -0,0 +1,273 @@
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/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 := 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
}