Skip to content

Commit 70af8be

Browse files
feat: add AddOrganizationMembers RPC handler with superadmin auth
Wire the membership package into the AddOrganizationMembers AdminService RPC. Batch endpoint accepts list of {user_id, role_id} pairs and returns per-member success/error results. - Handler iterates members, calls membershipService.AddOrganizationMember - Domain errors (already member, invalid role, etc.) returned as-is - Internal errors masked with generic message and logged server-side - Authorization: IsSuperUser (AdminService) - Proto regenerated from proton branch with new RPC Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 98d72e3 commit 70af8be

File tree

9 files changed

+1232
-817
lines changed

9 files changed

+1232
-817
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ TAG := $(shell git rev-list --tags --max-count=1)
44
VERSION := $(shell git describe --tags ${TAG})
55
.PHONY: build check fmt lint test test-race vet test-cover-html help install proto admin-app compose-up-dev
66
.DEFAULT_GOAL := build
7-
PROTON_COMMIT := "ac2df1932fcddcd7f7ff1af0ded07b14d73b781b"
7+
PROTON_COMMIT := "f7c40abb4fb97717bb523a8ce6225b45baab1ca7"
88

99
admin-app:
1010
@echo " > generating admin build"

cmd/serve.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import (
9292
"github.com/go-webauthn/webauthn/webauthn"
9393
"github.com/raystack/frontier/config"
9494
"github.com/raystack/frontier/core/group"
95+
"github.com/raystack/frontier/core/membership"
9596
"github.com/raystack/frontier/core/namespace"
9697
"github.com/raystack/frontier/core/organization"
9798
"github.com/raystack/frontier/core/policy"
@@ -420,6 +421,8 @@ func buildAPIDependencies(
420421
organizationService := organization.NewService(organizationRepository, relationService, userService,
421422
authnService, policyService, preferenceService, auditRecordRepository, roleService)
422423

424+
membershipService := membership.NewService(policyService, relationService, roleService, organizationService, userService, auditRecordRepository)
425+
423426
orgKycRepository := postgres.NewOrgKycRepository(dbc)
424427
orgKycService := kyc.NewService(orgKycRepository)
425428

@@ -620,6 +623,7 @@ func buildAPIDependencies(
620623
UserProjectsService: userProjectsService,
621624
AuditRecordService: auditRecordService,
622625
UserPATService: userPATService,
626+
MembershipService: membershipService,
623627
}
624628
return dependencies, nil
625629
}

internal/api/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/raystack/frontier/core/group"
3232
"github.com/raystack/frontier/core/invitation"
3333
"github.com/raystack/frontier/core/kyc"
34+
"github.com/raystack/frontier/core/membership"
3435
"github.com/raystack/frontier/core/metaschema"
3536
"github.com/raystack/frontier/core/namespace"
3637
"github.com/raystack/frontier/core/organization"
@@ -101,4 +102,5 @@ type Deps struct {
101102

102103
AuditRecordService *auditrecord.Service
103104
UserPATService *userpat.Service
105+
MembershipService *membership.Service
104106
}

internal/api/v1beta1connect/interfaces.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ type AuditRecordService interface {
407407
Export(ctx context.Context, query *rql.Query) (io.Reader, string, error)
408408
}
409409

410+
type MembershipService interface {
411+
AddOrganizationMember(ctx context.Context, orgID, principalID, principalType, roleID string) error
412+
}
413+
410414
type UserPATService interface {
411415
ValidateExpiry(expiresAt time.Time) error
412416
Create(ctx context.Context, req userpat.CreateRequest) (models.PAT, string, error)

internal/api/v1beta1connect/organization.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"connectrpc.com/connect"
77
grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
88
"github.com/raystack/frontier/core/audit"
9+
"github.com/raystack/frontier/core/membership"
910
"github.com/raystack/frontier/core/organization"
1011
"github.com/raystack/frontier/core/policy"
1112
"github.com/raystack/frontier/core/project"
@@ -569,6 +570,65 @@ func (h *ConnectHandler) SetOrganizationMemberRole(ctx context.Context, request
569570
return connect.NewResponse(&frontierv1beta1.SetOrganizationMemberRoleResponse{}), nil
570571
}
571572

573+
func (h *ConnectHandler) AddOrganizationMembers(ctx context.Context, request *connect.Request[frontierv1beta1.AddOrganizationMembersRequest]) (*connect.Response[frontierv1beta1.AddOrganizationMembersResponse], error) {
574+
errorLogger := NewErrorLogger()
575+
orgID := request.Msg.GetOrgId()
576+
577+
var results []*frontierv1beta1.OrgMemberResult
578+
for _, member := range request.Msg.GetMembers() {
579+
result := &frontierv1beta1.OrgMemberResult{
580+
UserId: member.GetUserId(),
581+
}
582+
583+
if err := h.membershipService.AddOrganizationMember(ctx, orgID, member.GetUserId(), schema.UserPrincipal, member.GetRoleId()); err != nil {
584+
result.Success = false
585+
result.Error = toClientError(err)
586+
if !isDomainError(err) {
587+
errorLogger.LogServiceError(ctx, request, "AddOrganizationMembers", err,
588+
zap.String("org_id", orgID),
589+
zap.String("user_id", member.GetUserId()),
590+
zap.String("role_id", member.GetRoleId()))
591+
}
592+
} else {
593+
result.Success = true
594+
}
595+
596+
results = append(results, result)
597+
}
598+
599+
return connect.NewResponse(&frontierv1beta1.AddOrganizationMembersResponse{
600+
Results: results,
601+
}), nil
602+
}
603+
604+
// isDomainError returns true if the error is a known domain error safe to expose to clients.
605+
func isDomainError(err error) bool {
606+
knownErrors := []error{
607+
membership.ErrAlreadyMember,
608+
membership.ErrInvalidOrgRole,
609+
organization.ErrNotExist,
610+
organization.ErrDisabled,
611+
user.ErrNotExist,
612+
user.ErrDisabled,
613+
role.ErrNotExist,
614+
role.ErrInvalidID,
615+
}
616+
for _, known := range knownErrors {
617+
if errors.Is(err, known) {
618+
return true
619+
}
620+
}
621+
return false
622+
}
623+
624+
// toClientError returns a client-safe error message.
625+
func toClientError(err error) string {
626+
if isDomainError(err) {
627+
return err.Error()
628+
}
629+
return ErrInternalServerError.Error()
630+
}
631+
572632
func (h *ConnectHandler) EnableOrganization(ctx context.Context, request *connect.Request[frontierv1beta1.EnableOrganizationRequest]) (*connect.Response[frontierv1beta1.EnableOrganizationResponse], error) {
573633
errorLogger := NewErrorLogger()
574634

internal/api/v1beta1connect/v1beta1connect.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type ConnectHandler struct {
6161
userProjectsService UserProjectsService
6262
auditRecordService AuditRecordService
6363
userPATService UserPATService
64+
membershipService MembershipService
6465
}
6566

6667
func NewConnectHandler(deps api.Deps, authConf authenticate.Config) *ConnectHandler {
@@ -111,6 +112,7 @@ func NewConnectHandler(deps api.Deps, authConf authenticate.Config) *ConnectHand
111112
userProjectsService: deps.UserProjectsService,
112113
auditRecordService: deps.AuditRecordService,
113114
userPATService: deps.UserPATService,
115+
membershipService: deps.MembershipService,
114116
}
115117
}
116118

pkg/server/connect_interceptors/authorization.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,9 @@ var authorizationValidationMap = map[string]func(ctx context.Context, handler *v
10161016
"/raystack.frontier.v1beta1.AdminService/AdminCreateOrganization": func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
10171017
return handler.IsSuperUser(ctx, req)
10181018
},
1019+
"/raystack.frontier.v1beta1.AdminService/AddOrganizationMembers": func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
1020+
return handler.IsSuperUser(ctx, req)
1021+
},
10191022
"/raystack.frontier.v1beta1.AdminService/SearchOrganizationUsers": func(ctx context.Context, handler *v1beta1connect.ConnectHandler, req connect.AnyRequest) error {
10201023
return handler.IsSuperUser(ctx, req)
10211024
},

0 commit comments

Comments
 (0)