Files
metacrypt/internal/grpcserver/engine.go
Kyle Isom 310ed83f28 Migrate gRPC server to mcdsl grpcserver package
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>
2026-03-26 14:42:41 -07:00

93 lines
2.8 KiB
Go

package grpcserver
import (
"context"
"errors"
"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"
)
type engineServer struct {
pb.UnimplementedEngineServiceServer
s *GRPCServer
}
func (es *engineServer) Mount(ctx context.Context, req *pb.MountRequest) (*pb.MountResponse, error) {
if req.Name == "" || req.Type == "" {
return nil, status.Error(codes.InvalidArgument, "name and type are required")
}
var config map[string]interface{}
if len(req.Config) > 0 {
config = make(map[string]interface{}, len(req.Config))
for k, v := range req.Config {
config[k] = v
}
}
// Inject external_url into engine config if available and not already set.
if config == nil {
config = make(map[string]interface{})
}
if _, ok := config["external_url"]; !ok && es.s.cfg.Server.ExternalURL != "" {
config["external_url"] = es.s.cfg.Server.ExternalURL
}
if err := es.s.engines.Mount(ctx, req.Name, engine.EngineType(req.Type), config); err != nil {
es.s.logger.Error("grpc: mount engine", "name", req.Name, "type", req.Type, "error", err)
switch {
case errors.Is(err, engine.ErrMountExists):
return nil, status.Error(codes.AlreadyExists, err.Error())
case errors.Is(err, engine.ErrUnknownType):
return nil, status.Error(codes.InvalidArgument, err.Error())
default:
return nil, status.Error(codes.Internal, err.Error())
}
}
ti := auth.TokenInfoFromContext(ctx)
username := ""
if ti != nil {
username = ti.Username
}
es.s.logger.Info("audit: engine mounted", "name", req.Name, "type", req.Type, "username", username)
return &pb.MountResponse{}, nil
}
func (es *engineServer) Unmount(ctx context.Context, req *pb.UnmountRequest) (*pb.UnmountResponse, error) {
if req.Name == "" {
return nil, status.Error(codes.InvalidArgument, "name is required")
}
if err := es.s.engines.Unmount(ctx, req.Name); err != nil {
if errors.Is(err, engine.ErrMountNotFound) {
return nil, status.Error(codes.NotFound, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
ti := auth.TokenInfoFromContext(ctx)
username := ""
if ti != nil {
username = ti.Username
}
es.s.logger.Info("audit: engine unmounted", "name", req.Name, "username", username)
return &pb.UnmountResponse{}, nil
}
func (es *engineServer) ListMounts(_ context.Context, _ *pb.ListMountsRequest) (*pb.ListMountsResponse, error) {
mounts := es.s.engines.ListMounts()
pbMounts := make([]*pb.MountInfo, 0, len(mounts))
for _, m := range mounts {
pbMounts = append(pbMounts, &pb.MountInfo{
Name: m.Name,
Type: string(m.Type),
MountPath: m.MountPath,
})
}
return &pb.ListMountsResponse{Mounts: pbMounts}, nil
}