Skip to content

Commit 714efb4

Browse files
committed
feat(grpc): admin RPCs for service accounts and trusted issuers
- 11 admin RPCs (create/update/delete/rotate/get/list service accounts, add/update/delete/get/list trusted issuers) over gRPC + REST gateway - client secret surfaced only via CreateServiceAccountResponse; the ServiceAccount proto message has no secret field so get/list/update cannot leak it - thin handlers delegate to the existing AdminProvider; projections mirror the webhook pattern
1 parent 40fe319 commit 714efb4

9 files changed

Lines changed: 5252 additions & 491 deletions

File tree

gen/go/authorizer/v1/admin.pb.go

Lines changed: 2210 additions & 422 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/go/authorizer/v1/admin.pb.gw.go

Lines changed: 883 additions & 36 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/go/authorizer/v1/admin_grpc.pb.go

Lines changed: 502 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gen/openapi/authorizer.swagger.json

Lines changed: 718 additions & 1 deletion
Large diffs are not rendered by default.

internal/grpcsrv/handlers/admin.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,164 @@ func (h *AdminHandler) FgaReset(ctx context.Context, _ *authorizerv1.FgaResetReq
439439
}
440440
return &authorizerv1.FgaResetResponse{Message: res.Message}, nil
441441
}
442+
443+
// CreateServiceAccount delegates to service.CreateServiceAccount and returns the
444+
// generated client secret exactly once (CreateServiceAccountResponse is the only
445+
// admin message that carries a secret). Requires super-admin auth.
446+
func (h *AdminHandler) CreateServiceAccount(ctx context.Context, req *authorizerv1.CreateServiceAccountRequest) (*authorizerv1.CreateServiceAccountResponse, error) {
447+
res, _, err := h.Service.CreateServiceAccount(ctx, transport.MetaFromGRPC(ctx), &model.CreateServiceAccountRequest{
448+
Name: req.GetName(),
449+
Description: req.Description,
450+
AllowedScopes: req.GetAllowedScopes(),
451+
})
452+
if err != nil {
453+
return nil, err
454+
}
455+
return &authorizerv1.CreateServiceAccountResponse{
456+
ServiceAccount: projectServiceAccount(res.ServiceAccount),
457+
ClientSecret: res.ClientSecret,
458+
}, nil
459+
}
460+
461+
// UpdateServiceAccount delegates to service.UpdateServiceAccount. Optional proto
462+
// fields map 1:1 onto the model's nullable pointers; the client secret is never
463+
// touched. Requires super-admin auth.
464+
func (h *AdminHandler) UpdateServiceAccount(ctx context.Context, req *authorizerv1.UpdateServiceAccountRequest) (*authorizerv1.UpdateServiceAccountResponse, error) {
465+
res, _, err := h.Service.UpdateServiceAccount(ctx, transport.MetaFromGRPC(ctx), &model.UpdateServiceAccountRequest{
466+
ID: req.GetId(),
467+
Name: req.Name,
468+
Description: req.Description,
469+
AllowedScopes: req.GetAllowedScopes(),
470+
IsActive: req.IsActive,
471+
})
472+
if err != nil {
473+
return nil, err
474+
}
475+
return &authorizerv1.UpdateServiceAccountResponse{ServiceAccount: projectServiceAccount(res)}, nil
476+
}
477+
478+
// DeleteServiceAccount delegates to service.DeleteServiceAccount, which cascades
479+
// to the account's trusted issuers. Requires super-admin auth.
480+
func (h *AdminHandler) DeleteServiceAccount(ctx context.Context, req *authorizerv1.DeleteServiceAccountRequest) (*authorizerv1.DeleteServiceAccountResponse, error) {
481+
res, _, err := h.Service.DeleteServiceAccount(ctx, transport.MetaFromGRPC(ctx), &model.ServiceAccountRequest{
482+
ID: req.GetId(),
483+
})
484+
if err != nil {
485+
return nil, err
486+
}
487+
return &authorizerv1.DeleteServiceAccountResponse{Message: res.Message}, nil
488+
}
489+
490+
// RotateServiceAccountSecret delegates to service.RotateServiceAccountSecret and
491+
// returns the new client secret exactly once (reusing CreateServiceAccountResponse).
492+
// Requires super-admin auth.
493+
func (h *AdminHandler) RotateServiceAccountSecret(ctx context.Context, req *authorizerv1.RotateServiceAccountSecretRequest) (*authorizerv1.CreateServiceAccountResponse, error) {
494+
res, _, err := h.Service.RotateServiceAccountSecret(ctx, transport.MetaFromGRPC(ctx), &model.ServiceAccountRequest{
495+
ID: req.GetId(),
496+
})
497+
if err != nil {
498+
return nil, err
499+
}
500+
return &authorizerv1.CreateServiceAccountResponse{
501+
ServiceAccount: projectServiceAccount(res.ServiceAccount),
502+
ClientSecret: res.ClientSecret,
503+
}, nil
504+
}
505+
506+
// GetServiceAccount delegates to service.ServiceAccount and projects the result.
507+
// The client secret is never surfaced. Requires super-admin auth.
508+
func (h *AdminHandler) GetServiceAccount(ctx context.Context, req *authorizerv1.GetServiceAccountRequest) (*authorizerv1.GetServiceAccountResponse, error) {
509+
res, _, err := h.Service.ServiceAccount(ctx, transport.MetaFromGRPC(ctx), &model.ServiceAccountRequest{
510+
ID: req.GetId(),
511+
})
512+
if err != nil {
513+
return nil, err
514+
}
515+
return &authorizerv1.GetServiceAccountResponse{ServiceAccount: projectServiceAccount(res)}, nil
516+
}
517+
518+
// ServiceAccounts delegates to service.ServiceAccounts and projects the
519+
// paginated result. Client secrets are never surfaced. Requires super-admin auth.
520+
func (h *AdminHandler) ServiceAccounts(ctx context.Context, req *authorizerv1.ServiceAccountsRequest) (*authorizerv1.ServiceAccountsResponse, error) {
521+
res, _, err := h.Service.ServiceAccounts(ctx, transport.MetaFromGRPC(ctx), &model.ListServiceAccountsRequest{
522+
Pagination: modelPaginatedRequest(req.GetPagination()),
523+
})
524+
if err != nil {
525+
return nil, err
526+
}
527+
return projectServiceAccounts(res), nil
528+
}
529+
530+
// AddTrustedIssuer delegates to service.AddTrustedIssuer. subject_claim defaults
531+
// to "sub" in the service layer when unset. Requires super-admin auth.
532+
func (h *AdminHandler) AddTrustedIssuer(ctx context.Context, req *authorizerv1.AddTrustedIssuerRequest) (*authorizerv1.AddTrustedIssuerResponse, error) {
533+
res, _, err := h.Service.AddTrustedIssuer(ctx, transport.MetaFromGRPC(ctx), &model.AddTrustedIssuerRequest{
534+
ServiceAccountID: req.GetServiceAccountId(),
535+
Name: req.GetName(),
536+
IssuerURL: req.GetIssuerUrl(),
537+
KeySourceType: req.GetKeySourceType(),
538+
JwksURL: req.JwksUrl,
539+
ExpectedAud: req.GetExpectedAud(),
540+
SubjectClaim: req.SubjectClaim,
541+
IssuerType: req.GetIssuerType(),
542+
SpiffeRefreshHintSeconds: req.SpiffeRefreshHintSeconds,
543+
})
544+
if err != nil {
545+
return nil, err
546+
}
547+
return &authorizerv1.AddTrustedIssuerResponse{TrustedIssuer: projectTrustedIssuer(res)}, nil
548+
}
549+
550+
// UpdateTrustedIssuer delegates to service.UpdateTrustedIssuer. Optional proto
551+
// fields map 1:1 onto the model's nullable pointers. Requires super-admin auth.
552+
func (h *AdminHandler) UpdateTrustedIssuer(ctx context.Context, req *authorizerv1.UpdateTrustedIssuerRequest) (*authorizerv1.UpdateTrustedIssuerResponse, error) {
553+
res, _, err := h.Service.UpdateTrustedIssuer(ctx, transport.MetaFromGRPC(ctx), &model.UpdateTrustedIssuerRequest{
554+
ID: req.GetId(),
555+
Name: req.Name,
556+
JwksURL: req.JwksUrl,
557+
ExpectedAud: req.ExpectedAud,
558+
IsActive: req.IsActive,
559+
SpiffeRefreshHintSeconds: req.SpiffeRefreshHintSeconds,
560+
})
561+
if err != nil {
562+
return nil, err
563+
}
564+
return &authorizerv1.UpdateTrustedIssuerResponse{TrustedIssuer: projectTrustedIssuer(res)}, nil
565+
}
566+
567+
// DeleteTrustedIssuer delegates to service.DeleteTrustedIssuer. Requires
568+
// super-admin auth.
569+
func (h *AdminHandler) DeleteTrustedIssuer(ctx context.Context, req *authorizerv1.DeleteTrustedIssuerRequest) (*authorizerv1.DeleteTrustedIssuerResponse, error) {
570+
res, _, err := h.Service.DeleteTrustedIssuer(ctx, transport.MetaFromGRPC(ctx), &model.TrustedIssuerRequest{
571+
ID: req.GetId(),
572+
})
573+
if err != nil {
574+
return nil, err
575+
}
576+
return &authorizerv1.DeleteTrustedIssuerResponse{Message: res.Message}, nil
577+
}
578+
579+
// GetTrustedIssuer delegates to service.TrustedIssuer and projects the result.
580+
// Requires super-admin auth.
581+
func (h *AdminHandler) GetTrustedIssuer(ctx context.Context, req *authorizerv1.GetTrustedIssuerRequest) (*authorizerv1.GetTrustedIssuerResponse, error) {
582+
res, _, err := h.Service.TrustedIssuer(ctx, transport.MetaFromGRPC(ctx), &model.TrustedIssuerRequest{
583+
ID: req.GetId(),
584+
})
585+
if err != nil {
586+
return nil, err
587+
}
588+
return &authorizerv1.GetTrustedIssuerResponse{TrustedIssuer: projectTrustedIssuer(res)}, nil
589+
}
590+
591+
// TrustedIssuers delegates to service.TrustedIssuers and projects the paginated
592+
// result. service_account_id is optional. Requires super-admin auth.
593+
func (h *AdminHandler) TrustedIssuers(ctx context.Context, req *authorizerv1.TrustedIssuersRequest) (*authorizerv1.TrustedIssuersResponse, error) {
594+
res, _, err := h.Service.TrustedIssuers(ctx, transport.MetaFromGRPC(ctx), &model.ListTrustedIssuersRequest{
595+
ServiceAccountID: req.ServiceAccountId,
596+
Pagination: modelPaginatedRequest(req.GetPagination()),
597+
})
598+
if err != nil {
599+
return nil, err
600+
}
601+
return projectTrustedIssuers(res), nil
602+
}

internal/grpcsrv/handlers/admin_project.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,78 @@ func projectFgaExpandResponse(r *model.FgaExpandResponse) *authorizerv1.FgaExpan
391391
}
392392
return &authorizerv1.FgaExpandResponse{Tree: r.Tree}
393393
}
394+
395+
// projectServiceAccount converts a single GraphQL ServiceAccount model into the
396+
// proto message. There is deliberately NO client-secret field on the proto
397+
// ServiceAccount: the plaintext secret is surfaced only by
398+
// CreateServiceAccountResponse, so no get/list/update path can leak it.
399+
func projectServiceAccount(s *model.ServiceAccount) *authorizerv1.ServiceAccount {
400+
if s == nil {
401+
return nil
402+
}
403+
return &authorizerv1.ServiceAccount{
404+
Id: s.ID,
405+
Name: s.Name,
406+
Description: refs.StringValue(s.Description),
407+
AllowedScopes: s.AllowedScopes,
408+
IsActive: s.IsActive,
409+
CreatedAt: refs.Int64Value(s.CreatedAt),
410+
UpdatedAt: refs.Int64Value(s.UpdatedAt),
411+
}
412+
}
413+
414+
// projectServiceAccounts converts the GraphQL ServiceAccounts model (a page plus
415+
// its pagination cursor) into the proto response.
416+
func projectServiceAccounts(s *model.ServiceAccounts) *authorizerv1.ServiceAccountsResponse {
417+
if s == nil {
418+
return &authorizerv1.ServiceAccountsResponse{}
419+
}
420+
accounts := make([]*authorizerv1.ServiceAccount, 0, len(s.ServiceAccounts))
421+
for _, item := range s.ServiceAccounts {
422+
accounts = append(accounts, projectServiceAccount(item))
423+
}
424+
return &authorizerv1.ServiceAccountsResponse{
425+
ServiceAccounts: accounts,
426+
Pagination: projectPagination(s.Pagination),
427+
}
428+
}
429+
430+
// projectTrustedIssuer converts a single GraphQL TrustedIssuer model into the
431+
// proto message. Optional pointer fields collapse to zero values; the issuer
432+
// references its parent via service_account_id.
433+
func projectTrustedIssuer(t *model.TrustedIssuer) *authorizerv1.TrustedIssuer {
434+
if t == nil {
435+
return nil
436+
}
437+
return &authorizerv1.TrustedIssuer{
438+
Id: t.ID,
439+
ServiceAccountId: t.ServiceAccountID,
440+
Name: t.Name,
441+
IssuerUrl: t.IssuerURL,
442+
KeySourceType: t.KeySourceType,
443+
JwksUrl: refs.StringValue(t.JwksURL),
444+
ExpectedAud: t.ExpectedAud,
445+
SubjectClaim: t.SubjectClaim,
446+
IssuerType: t.IssuerType,
447+
IsActive: t.IsActive,
448+
SpiffeRefreshHintSeconds: refs.Int64Value(t.SpiffeRefreshHintSeconds),
449+
CreatedAt: refs.Int64Value(t.CreatedAt),
450+
UpdatedAt: refs.Int64Value(t.UpdatedAt),
451+
}
452+
}
453+
454+
// projectTrustedIssuers converts the GraphQL TrustedIssuers model (a page plus
455+
// its pagination cursor) into the proto response.
456+
func projectTrustedIssuers(t *model.TrustedIssuers) *authorizerv1.TrustedIssuersResponse {
457+
if t == nil {
458+
return &authorizerv1.TrustedIssuersResponse{}
459+
}
460+
issuers := make([]*authorizerv1.TrustedIssuer, 0, len(t.TrustedIssuers))
461+
for _, item := range t.TrustedIssuers {
462+
issuers = append(issuers, projectTrustedIssuer(item))
463+
}
464+
return &authorizerv1.TrustedIssuersResponse{
465+
TrustedIssuers: issuers,
466+
Pagination: projectPagination(t.Pagination),
467+
}
468+
}

0 commit comments

Comments
 (0)