From 0ed8169bade2a391f2986b53b58e022b872efc8d Mon Sep 17 00:00:00 2001 From: Vikrant Gupta Date: Thu, 12 Mar 2026 15:12:50 +0530 Subject: [PATCH] feat(authz): add service account authz changes (#10567) --- ee/authz/openfgaschema/base.fga | 38 +++++++++++-------- .../src/hooks/useAuthZ/permissions.type.ts | 6 +-- pkg/authz/openfgaschema/base.fga | 4 +- .../implserviceaccount/module.go | 10 ++--- pkg/types/authtypes/relation.go | 17 +++++---- pkg/types/authtypes/selector.go | 16 +++++--- pkg/types/authtypes/typeable.go | 26 ++++++++----- .../authtypes/typeable_serviceaccount.go | 38 +++++++++++++++++++ 8 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 pkg/types/authtypes/typeable_serviceaccount.go diff --git a/ee/authz/openfgaschema/base.fga b/ee/authz/openfgaschema/base.fga index c28377cee6..5ef9f5b91d 100644 --- a/ee/authz/openfgaschema/base.fga +++ b/ee/authz/openfgaschema/base.fga @@ -2,39 +2,45 @@ module base type organisation relations - define read: [user, role#assignee] - define update: [user, role#assignee] + define read: [user, serviceaccount, role#assignee] + define update: [user, serviceaccount, role#assignee] type user relations - define read: [user, role#assignee] - define update: [user, role#assignee] - define delete: [user, role#assignee] + define read: [user, serviceaccount, role#assignee] + define update: [user, serviceaccount, role#assignee] + define delete: [user, serviceaccount, role#assignee] + +type serviceaccount + relations + define read: [user, serviceaccount, role#assignee] + define update: [user, serviceaccount, role#assignee] + define delete: [user, serviceaccount, role#assignee] type anonymous type role relations - define assignee: [user, anonymous] + define assignee: [user, serviceaccount, anonymous] - define read: [user, role#assignee] - define update: [user, role#assignee] - define delete: [user, role#assignee] + define read: [user, serviceaccount, role#assignee] + define update: [user, serviceaccount, role#assignee] + define delete: [user, serviceaccount, role#assignee] type metaresources relations - define create: [user, role#assignee] - define list: [user, role#assignee] + define create: [user, serviceaccount, role#assignee] + define list: [user, serviceaccount, role#assignee] type metaresource relations - define read: [user, anonymous, role#assignee] - define update: [user, role#assignee] - define delete: [user, role#assignee] + define read: [user, serviceaccount, anonymous, role#assignee] + define update: [user, serviceaccount, role#assignee] + define delete: [user, serviceaccount, role#assignee] - define block: [user, role#assignee] + define block: [user, serviceaccount, role#assignee] type telemetryresource relations - define read: [user, role#assignee] + define read: [user, serviceaccount, role#assignee] \ No newline at end of file diff --git a/frontend/src/hooks/useAuthZ/permissions.type.ts b/frontend/src/hooks/useAuthZ/permissions.type.ts index fe47165ad7..447a5facab 100644 --- a/frontend/src/hooks/useAuthZ/permissions.type.ts +++ b/frontend/src/hooks/useAuthZ/permissions.type.ts @@ -23,10 +23,10 @@ export default { relations: { assignee: ['role'], create: ['metaresources'], - delete: ['user', 'role', 'organization', 'metaresource'], + delete: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'], list: ['metaresources'], - read: ['user', 'role', 'organization', 'metaresource'], - update: ['user', 'role', 'organization', 'metaresource'], + read: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'], + update: ['user', 'serviceaccount', 'role', 'organization', 'metaresource'], }, }, } as const; diff --git a/pkg/authz/openfgaschema/base.fga b/pkg/authz/openfgaschema/base.fga index 192009b751..fa13e9e615 100644 --- a/pkg/authz/openfgaschema/base.fga +++ b/pkg/authz/openfgaschema/base.fga @@ -2,9 +2,11 @@ module base type user +type serviceaccount + type role relations - define assignee: [user] + define assignee: [user, serviceaccount] type organisation relations diff --git a/pkg/modules/serviceaccount/implserviceaccount/module.go b/pkg/modules/serviceaccount/implserviceaccount/module.go index 53bc752334..7cde1e0765 100644 --- a/pkg/modules/serviceaccount/implserviceaccount/module.go +++ b/pkg/modules/serviceaccount/implserviceaccount/module.go @@ -33,7 +33,7 @@ func (module *module) Create(ctx context.Context, orgID valuer.UUID, serviceAcco } // authz actions cannot run in sql transactions - err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil)) + err = module.authz.Grant(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil)) if err != nil { return err } @@ -138,7 +138,7 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, input *serv // gets the role diff if any to modify grants. grants, revokes := serviceAccount.PatchRoles(input) - err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil)) + err = module.authz.ModifyGrant(ctx, orgID, revokes, grants, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil)) if err != nil { return err } @@ -203,7 +203,7 @@ func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.U } // revoke from authz first as this cannot run in sql transaction - err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, serviceAccount.ID.String(), orgID, nil)) + err = module.authz.Revoke(ctx, orgID, serviceAccount.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, serviceAccount.ID.String(), orgID, nil)) if err != nil { return err } @@ -309,7 +309,7 @@ func (module *module) RevokeFactorAPIKey(ctx context.Context, serviceAccountID v } func (module *module) disableServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error { - err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil)) + err := module.authz.Revoke(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, input.ID.String(), orgID, nil)) if err != nil { return err } @@ -337,7 +337,7 @@ func (module *module) disableServiceAccount(ctx context.Context, orgID valuer.UU } func (module *module) activateServiceAccount(ctx context.Context, orgID valuer.UUID, input *serviceaccounttypes.ServiceAccount) error { - err := module.authz.Grant(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableUser, input.ID.String(), orgID, nil)) + err := module.authz.Grant(ctx, orgID, input.Roles, authtypes.MustNewSubject(authtypes.TypeableServiceAccount, input.ID.String(), orgID, nil)) if err != nil { return err } diff --git a/pkg/types/authtypes/relation.go b/pkg/types/authtypes/relation.go index c6735c5911..46e9c1841c 100644 --- a/pkg/types/authtypes/relation.go +++ b/pkg/types/authtypes/relation.go @@ -20,19 +20,20 @@ var ( ) var TypeableRelations = map[Type][]Relation{ - TypeUser: {RelationRead, RelationUpdate, RelationDelete}, - TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete}, - TypeOrganization: {RelationRead, RelationUpdate, RelationDelete}, - TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete}, - TypeMetaResources: {RelationCreate, RelationList}, + TypeUser: {RelationRead, RelationUpdate, RelationDelete}, + TypeServiceAccount: {RelationRead, RelationUpdate, RelationDelete}, + TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete}, + TypeOrganization: {RelationRead, RelationUpdate, RelationDelete}, + TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete}, + TypeMetaResources: {RelationCreate, RelationList}, } var RelationsTypeable = map[Relation][]Type{ RelationCreate: {TypeMetaResources}, - RelationRead: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource}, + RelationRead: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource}, RelationList: {TypeMetaResources}, - RelationUpdate: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource}, - RelationDelete: {TypeUser, TypeRole, TypeOrganization, TypeMetaResource}, + RelationUpdate: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource}, + RelationDelete: {TypeUser, TypeServiceAccount, TypeRole, TypeOrganization, TypeMetaResource}, RelationAssignee: {TypeRole}, } diff --git a/pkg/types/authtypes/selector.go b/pkg/types/authtypes/selector.go index dd243902ca..587a7688b4 100644 --- a/pkg/types/authtypes/selector.go +++ b/pkg/types/authtypes/selector.go @@ -23,11 +23,12 @@ var ( ) var ( - typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) - typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`) - typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`) - typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) - typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) + typeUserSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) + typeServiceAccountSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) + typeRoleSelectorRegex = regexp.MustCompile(`^([a-z-]{1,50}|\*)$`) + typeAnonymousSelectorRegex = regexp.MustCompile(`^\*$`) + typeOrganizationSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) + typeMetaResourceSelectorRegex = regexp.MustCompile(`^(^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$|\*)$`) // metaresources selectors are used to select either all or none until we introduce some hierarchy here. typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`) ) @@ -98,6 +99,11 @@ func IsValidSelector(typed Type, selector string) error { return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeUserSelectorRegex.String()) } return nil + case TypeServiceAccount: + if !typeServiceAccountSelectorRegex.MatchString(selector) { + return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeServiceAccountSelectorRegex.String()) + } + return nil case TypeRole: if !typeRoleSelectorRegex.MatchString(selector) { return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String()) diff --git a/pkg/types/authtypes/typeable.go b/pkg/types/authtypes/typeable.go index 0bb2b6c3d5..bbf36a918f 100644 --- a/pkg/types/authtypes/typeable.go +++ b/pkg/types/authtypes/typeable.go @@ -15,19 +15,21 @@ var ( ) var ( - TypeUser = Type{valuer.NewString("user")} - TypeAnonymous = Type{valuer.NewString("anonymous")} - TypeRole = Type{valuer.NewString("role")} - TypeOrganization = Type{valuer.NewString("organization")} - TypeMetaResource = Type{valuer.NewString("metaresource")} - TypeMetaResources = Type{valuer.NewString("metaresources")} + TypeUser = Type{valuer.NewString("user")} + TypeServiceAccount = Type{valuer.NewString("serviceaccount")} + TypeAnonymous = Type{valuer.NewString("anonymous")} + TypeRole = Type{valuer.NewString("role")} + TypeOrganization = Type{valuer.NewString("organization")} + TypeMetaResource = Type{valuer.NewString("metaresource")} + TypeMetaResources = Type{valuer.NewString("metaresources")} ) var ( - TypeableUser = &typeableUser{} - TypeableAnonymous = &typeableAnonymous{} - TypeableRole = &typeableRole{} - TypeableOrganization = &typeableOrganization{} + TypeableUser = &typeableUser{} + TypeableServiceAccount = &typeableServiceAccount{} + TypeableAnonymous = &typeableAnonymous{} + TypeableRole = &typeableRole{} + TypeableOrganization = &typeableOrganization{} ) type Typeable interface { @@ -53,6 +55,8 @@ func NewType(input string) (Type, error) { switch input { case "user": return TypeUser, nil + case "serviceaccount": + return TypeServiceAccount, nil case "role": return TypeRole, nil case "organization": @@ -88,6 +92,8 @@ func NewTypeableFromType(typed Type, name Name) (Typeable, error) { return TypeableRole, nil case TypeUser: return TypeableUser, nil + case TypeServiceAccount: + return TypeableServiceAccount, nil case TypeOrganization: return TypeableOrganization, nil case TypeMetaResource: diff --git a/pkg/types/authtypes/typeable_serviceaccount.go b/pkg/types/authtypes/typeable_serviceaccount.go new file mode 100644 index 0000000000..fafef8f134 --- /dev/null +++ b/pkg/types/authtypes/typeable_serviceaccount.go @@ -0,0 +1,38 @@ +package authtypes + +import ( + "github.com/SigNoz/signoz/pkg/valuer" + openfgav1 "github.com/openfga/api/proto/openfga/v1" +) + +var _ Typeable = new(typeableServiceAccount) + +type typeableServiceAccount struct{} + +func (typeableServiceAccount *typeableServiceAccount) Tuples(subject string, relation Relation, selectors []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) { + tuples := make([]*openfgav1.TupleKey, 0) + + for _, selector := range selectors { + object := typeableServiceAccount.Prefix(orgID) + "/" + selector.String() + tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object}) + } + + return tuples, nil +} + +func (typeableServiceAccount *typeableServiceAccount) Type() Type { + return TypeServiceAccount +} + +func (typeableServiceAccount *typeableServiceAccount) Name() Name { + return MustNewName("serviceaccount") +} + +// example: serviceaccount:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/serviceaccount +func (typeableServiceAccount *typeableServiceAccount) Prefix(orgID valuer.UUID) string { + return typeableServiceAccount.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableServiceAccount.Name().String() +} + +func (typeableServiceAccount *typeableServiceAccount) Scope(relation Relation) string { + return typeableServiceAccount.Name().String() + ":" + relation.StringValue() +}