// ssoclientservice implements mciasv1.SSOClientServiceServer. // All handlers are admin-only and delegate to the same db package used by // the REST SSO client handlers in internal/server/handlers_sso_clients.go. package grpcserver import ( "context" "errors" "fmt" "time" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" mciasv1 "git.wntrmute.dev/mc/mcias/gen/mcias/v1" "git.wntrmute.dev/mc/mcias/internal/db" "git.wntrmute.dev/mc/mcias/internal/model" ) type ssoClientServiceServer struct { mciasv1.UnimplementedSSOClientServiceServer s *Server } func ssoClientToProto(c *model.SSOClient) *mciasv1.SSOClient { return &mciasv1.SSOClient{ ClientId: c.ClientID, RedirectUri: c.RedirectURI, Tags: c.Tags, Enabled: c.Enabled, CreatedAt: c.CreatedAt.UTC().Format(time.RFC3339), UpdatedAt: c.UpdatedAt.UTC().Format(time.RFC3339), } } func (ss *ssoClientServiceServer) ListSSOClients(ctx context.Context, _ *mciasv1.ListSSOClientsRequest) (*mciasv1.ListSSOClientsResponse, error) { if err := ss.s.requireAdmin(ctx); err != nil { return nil, err } clients, err := ss.s.db.ListSSOClients() if err != nil { ss.s.logger.Error("list SSO clients", "error", err) return nil, status.Error(codes.Internal, "internal error") } resp := &mciasv1.ListSSOClientsResponse{ Clients: make([]*mciasv1.SSOClient, 0, len(clients)), } for _, c := range clients { resp.Clients = append(resp.Clients, ssoClientToProto(c)) } return resp, nil } func (ss *ssoClientServiceServer) CreateSSOClient(ctx context.Context, req *mciasv1.CreateSSOClientRequest) (*mciasv1.CreateSSOClientResponse, error) { if err := ss.s.requireAdmin(ctx); err != nil { return nil, err } if req.ClientId == "" { return nil, status.Error(codes.InvalidArgument, "client_id is required") } if req.RedirectUri == "" { return nil, status.Error(codes.InvalidArgument, "redirect_uri is required") } claims := claimsFromContext(ctx) var createdBy *int64 if claims != nil { if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil { createdBy = &actor.ID } } c, err := ss.s.db.CreateSSOClient(req.ClientId, req.RedirectUri, req.Tags, createdBy) if err != nil { ss.s.logger.Error("create SSO client", "error", err) return nil, status.Error(codes.InvalidArgument, err.Error()) } ss.s.db.WriteAuditEvent(model.EventSSOClientCreated, createdBy, nil, peerIP(ctx), //nolint:errcheck fmt.Sprintf(`{"client_id":%q}`, c.ClientID)) return &mciasv1.CreateSSOClientResponse{Client: ssoClientToProto(c)}, nil } func (ss *ssoClientServiceServer) GetSSOClient(ctx context.Context, req *mciasv1.GetSSOClientRequest) (*mciasv1.GetSSOClientResponse, error) { if err := ss.s.requireAdmin(ctx); err != nil { return nil, err } if req.ClientId == "" { return nil, status.Error(codes.InvalidArgument, "client_id is required") } c, err := ss.s.db.GetSSOClient(req.ClientId) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "SSO client not found") } if err != nil { ss.s.logger.Error("get SSO client", "error", err) return nil, status.Error(codes.Internal, "internal error") } return &mciasv1.GetSSOClientResponse{Client: ssoClientToProto(c)}, nil } func (ss *ssoClientServiceServer) UpdateSSOClient(ctx context.Context, req *mciasv1.UpdateSSOClientRequest) (*mciasv1.UpdateSSOClientResponse, error) { if err := ss.s.requireAdmin(ctx); err != nil { return nil, err } if req.ClientId == "" { return nil, status.Error(codes.InvalidArgument, "client_id is required") } var redirectURI *string if req.RedirectUri != nil { v := req.GetRedirectUri() redirectURI = &v } var tags *[]string if req.UpdateTags { t := req.Tags tags = &t } var enabled *bool if req.Enabled != nil { v := req.GetEnabled() enabled = &v } err := ss.s.db.UpdateSSOClient(req.ClientId, redirectURI, tags, enabled) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "SSO client not found") } if err != nil { ss.s.logger.Error("update SSO client", "error", err) return nil, status.Error(codes.InvalidArgument, err.Error()) } claims := claimsFromContext(ctx) var actorID *int64 if claims != nil { if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil { actorID = &actor.ID } } ss.s.db.WriteAuditEvent(model.EventSSOClientUpdated, actorID, nil, peerIP(ctx), //nolint:errcheck fmt.Sprintf(`{"client_id":%q}`, req.ClientId)) updated, err := ss.s.db.GetSSOClient(req.ClientId) if err != nil { return nil, status.Error(codes.Internal, "internal error") } return &mciasv1.UpdateSSOClientResponse{Client: ssoClientToProto(updated)}, nil } func (ss *ssoClientServiceServer) DeleteSSOClient(ctx context.Context, req *mciasv1.DeleteSSOClientRequest) (*mciasv1.DeleteSSOClientResponse, error) { if err := ss.s.requireAdmin(ctx); err != nil { return nil, err } if req.ClientId == "" { return nil, status.Error(codes.InvalidArgument, "client_id is required") } err := ss.s.db.DeleteSSOClient(req.ClientId) if errors.Is(err, db.ErrNotFound) { return nil, status.Error(codes.NotFound, "SSO client not found") } if err != nil { ss.s.logger.Error("delete SSO client", "error", err) return nil, status.Error(codes.Internal, "internal error") } claims := claimsFromContext(ctx) var actorID *int64 if claims != nil { if actor, err := ss.s.db.GetAccountByUUID(claims.Subject); err == nil { actorID = &actor.ID } } ss.s.db.WriteAuditEvent(model.EventSSOClientDeleted, actorID, nil, peerIP(ctx), //nolint:errcheck fmt.Sprintf(`{"client_id":%q}`, req.ClientId)) return &mciasv1.DeleteSSOClientResponse{}, nil }