diff --git a/cmd/community/server.go b/cmd/community/server.go index c91b6fb0ee..2d264db59c 100644 --- a/cmd/community/server.go +++ b/cmd/community/server.go @@ -19,6 +19,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/query-service/app" "github.com/SigNoz/signoz/pkg/queryparser" @@ -80,12 +81,15 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] { return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx)) }, - func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Module, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module { + func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, _ role.Setter, _ role.Granter, queryParser queryparser.QueryParser, _ querier.Querier, _ licensing.Licensing) dashboard.Module { return impldashboard.NewModule(impldashboard.NewStore(store), settings, analytics, orgGetter, queryParser) }, func(_ licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] { return noopgateway.NewProviderFactory() }, + func(store sqlstore.SQLStore, authz authz.AuthZ, licensing licensing.Licensing, _ []role.RegisterTypeable) role.Setter { + return implrole.NewSetter(implrole.NewStore(store), authz) + }, ) if err != nil { logger.ErrorContext(ctx, "failed to create signoz", "error", err) diff --git a/cmd/enterprise/server.go b/cmd/enterprise/server.go index 52b50fe78e..330749da97 100644 --- a/cmd/enterprise/server.go +++ b/cmd/enterprise/server.go @@ -14,6 +14,7 @@ import ( enterpriselicensing "github.com/SigNoz/signoz/ee/licensing" "github.com/SigNoz/signoz/ee/licensing/httplicensing" "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard" + "github.com/SigNoz/signoz/ee/modules/role/implrole" enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app" "github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema" "github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore" @@ -29,6 +30,7 @@ import ( pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/role" + pkgimplrole "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/querier" "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/signoz" @@ -119,13 +121,17 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e func(ctx context.Context, sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] { return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx)) }, - func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module { - return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, role, queryParser, querier, licensing) + func(store sqlstore.SQLStore, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, roleSetter role.Setter, granter role.Granter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module { + return impldashboard.NewModule(pkgimpldashboard.NewStore(store), settings, analytics, orgGetter, roleSetter, granter, queryParser, querier, licensing) }, func(licensing licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config] { return httpgateway.NewProviderFactory(licensing) }, + func(store sqlstore.SQLStore, authz authz.AuthZ, licensing licensing.Licensing, registry []role.RegisterTypeable) role.Setter { + return implrole.NewSetter(pkgimplrole.NewStore(store), authz, licensing, registry) + }, ) + if err != nil { logger.ErrorContext(ctx, "failed to create signoz", "error", err) return err diff --git a/docs/api/openapi.yml b/docs/api/openapi.yml index 53d5fe8f25..78c9c8b829 100644 --- a/docs/api/openapi.yml +++ b/docs/api/openapi.yml @@ -209,7 +209,7 @@ paths: /api/v1/dashboards/{id}/public: delete: deprecated: false - description: This endpoints deletes the public sharing config and disables the + description: This endpoint deletes the public sharing config and disables the public sharing of a dashboard operationId: DeletePublicDashboard parameters: @@ -253,7 +253,7 @@ paths: - dashboard get: deprecated: false - description: This endpoints returns public sharing config for a dashboard + description: This endpoint returns public sharing config for a dashboard operationId: GetPublicDashboard parameters: - in: path @@ -301,7 +301,7 @@ paths: - dashboard post: deprecated: false - description: This endpoints creates public sharing config and enables public + description: This endpoint creates public sharing config and enables public sharing of the dashboard operationId: CreatePublicDashboard parameters: @@ -355,7 +355,7 @@ paths: - dashboard put: deprecated: false - description: This endpoints updates the public sharing config for a dashboard + description: This endpoint updates the public sharing config for a dashboard operationId: UpdatePublicDashboard parameters: - in: path @@ -671,7 +671,7 @@ paths: /api/v1/global/config: get: deprecated: false - description: This endpoints returns global config + description: This endpoint returns global config operationId: GetGlobalConfig responses: "200": @@ -1447,8 +1447,7 @@ paths: /api/v1/public/dashboards/{id}: get: deprecated: false - description: This endpoints returns the sanitized dashboard data for public - access + description: This endpoint returns the sanitized dashboard data for public access operationId: GetPublicDashboardData parameters: - in: path @@ -1579,6 +1578,228 @@ paths: summary: Reset password tags: - users + /api/v1/roles: + get: + deprecated: false + description: This endpoint lists all roles + operationId: ListRoles + responses: + "200": + content: + application/json: + schema: + properties: + data: + items: + $ref: '#/components/schemas/RoletypesRole' + type: array + status: + type: string + type: object + description: OK + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: List roles + tags: + - role + post: + deprecated: false + description: This endpoint creates a role + operationId: CreateRole + responses: + "201": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/TypesIdentifiable' + status: + type: string + type: object + description: Created + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Create role + tags: + - role + /api/v1/roles/{id}: + delete: + deprecated: false + description: This endpoint deletes a role + operationId: DeleteRole + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "204": + content: + application/json: + schema: + type: string + description: No Content + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Delete role + tags: + - role + get: + deprecated: false + description: This endpoint gets a role + operationId: GetRole + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "200": + content: + application/json: + schema: + properties: + data: + $ref: '#/components/schemas/RoletypesRole' + status: + type: string + type: object + description: OK + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Get role + tags: + - role + patch: + deprecated: false + description: This endpoint patches a role + operationId: PatchRole + parameters: + - in: path + name: id + required: true + schema: + type: string + responses: + "204": + content: + application/json: + schema: + type: string + description: No Content + "401": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Unauthorized + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Forbidden + "500": + content: + application/json: + schema: + $ref: '#/components/schemas/RenderErrorResponse' + description: Internal Server Error + security: + - api_key: + - ADMIN + - tokenizer: + - ADMIN + summary: Patch role + tags: + - role /api/v1/user: get: deprecated: false @@ -3888,6 +4109,25 @@ components: status: type: string type: object + RoletypesRole: + properties: + createdAt: + format: date-time + type: string + description: + type: string + id: + type: string + name: + type: string + orgId: + type: string + type: + type: string + updatedAt: + format: date-time + type: string + type: object TypesChangePasswordRequest: properties: newPassword: diff --git a/ee/authz/openfgaauthz/provider.go b/ee/authz/openfgaauthz/provider.go index 6034314b4a..b97aa22b04 100644 --- a/ee/authz/openfgaauthz/provider.go +++ b/ee/authz/openfgaauthz/provider.go @@ -47,7 +47,7 @@ func (provider *provider) Check(ctx context.Context, tuple *openfgav1.TupleKey) return provider.pkgAuthzService.Check(ctx, tuple) } -func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error { +func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error { subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) if err != nil { return err @@ -66,7 +66,7 @@ func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims aut return nil } -func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, _ authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error { +func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector, _ []authtypes.Selector) error { subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil) if err != nil { return err diff --git a/ee/modules/dashboard/impldashboard/module.go b/ee/modules/dashboard/impldashboard/module.go index 76697b56c9..2d3bbe2f12 100644 --- a/ee/modules/dashboard/impldashboard/module.go +++ b/ee/modules/dashboard/impldashboard/module.go @@ -26,12 +26,13 @@ type module struct { pkgDashboardModule dashboard.Module store dashboardtypes.Store settings factory.ScopedProviderSettings - role role.Module + roleSetter role.Setter + granter role.Granter querier querier.Querier licensing licensing.Licensing } -func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, role role.Module, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module { +func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, roleSetter role.Setter, granter role.Granter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing) dashboard.Module { scopedProviderSettings := factory.NewScopedProviderSettings(settings, "github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard") pkgDashboardModule := pkgimpldashboard.NewModule(store, settings, analytics, orgGetter, queryParser) @@ -39,7 +40,8 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an pkgDashboardModule: pkgDashboardModule, store: store, settings: scopedProviderSettings, - role: role, + roleSetter: roleSetter, + granter: granter, querier: querier, licensing: licensing, } @@ -59,12 +61,12 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi return errors.Newf(errors.TypeAlreadyExists, dashboardtypes.ErrCodePublicDashboardAlreadyExists, "dashboard with id %s is already public", storablePublicDashboard.DashboardID) } - role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID)) + role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID)) if err != nil { return err } - err = module.role.Assign(ctx, role.ID, orgID, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil)) + err = module.granter.Grant(ctx, orgID, roletypes.SigNozAnonymousRoleName, authtypes.MustNewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.StringValue(), orgID, nil)) if err != nil { return err } @@ -77,7 +79,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()), ) - err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil) + err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, []*authtypes.Object{additionObject}, nil) if err != nil { return err } @@ -193,7 +195,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb return err } - role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID)) + role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID)) if err != nil { return err } @@ -206,7 +208,7 @@ func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashb authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()), ) - err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject}) + err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject}) if err != nil { return err } @@ -270,7 +272,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb return err } - role, err := module.role.GetOrCreate(ctx, roletypes.NewRole(roletypes.AnonymousUserRoleName, roletypes.AnonymousUserRoleDescription, roletypes.RoleTypeManaged.StringValue(), orgID)) + role, err := module.roleSetter.GetOrCreate(ctx, orgID, roletypes.NewRole(roletypes.SigNozAnonymousRoleName, roletypes.SigNozAnonymousRoleDescription, roletypes.RoleTypeManaged, orgID)) if err != nil { return err } @@ -283,7 +285,7 @@ func (module *module) deletePublic(ctx context.Context, orgID valuer.UUID, dashb authtypes.MustNewSelector(authtypes.TypeMetaResource, publicDashboard.ID.String()), ) - err = module.role.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject}) + err = module.roleSetter.PatchObjects(ctx, orgID, role.ID, authtypes.RelationRead, nil, []*authtypes.Object{deletionObject}) if err != nil { return err } diff --git a/ee/modules/role/implrole/setter.go b/ee/modules/role/implrole/setter.go new file mode 100644 index 0000000000..e2e61b7806 --- /dev/null +++ b/ee/modules/role/implrole/setter.go @@ -0,0 +1,165 @@ +package implrole + +import ( + "context" + "slices" + + "github.com/SigNoz/signoz/pkg/authz" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/licensing" + "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type setter struct { + store roletypes.Store + authz authz.AuthZ + licensing licensing.Licensing + registry []role.RegisterTypeable +} + +func NewSetter(store roletypes.Store, authz authz.AuthZ, licensing licensing.Licensing, registry []role.RegisterTypeable) role.Setter { + return &setter{ + store: store, + authz: authz, + licensing: licensing, + registry: registry, + } +} + +func (setter *setter) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error { + _, err := setter.licensing.GetActive(ctx, orgID) + if err != nil { + return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error()) + } + + return setter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) +} + +func (setter *setter) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) { + _, err := setter.licensing.GetActive(ctx, orgID) + if err != nil { + return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error()) + } + + existingRole, err := setter.store.GetByOrgIDAndName(ctx, role.OrgID, role.Name) + if err != nil { + if !errors.Ast(err, errors.TypeNotFound) { + return nil, err + } + } + + if existingRole != nil { + return roletypes.NewRoleFromStorableRole(existingRole), nil + } + + err = setter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) + if err != nil { + return nil, err + } + + return role, nil +} + +func (setter *setter) GetResources(_ context.Context) []*authtypes.Resource { + typeables := make([]authtypes.Typeable, 0) + for _, register := range setter.registry { + typeables = append(typeables, register.MustGetTypeables()...) + } + // role module cannot self register itself! + typeables = append(typeables, setter.MustGetTypeables()...) + + resources := make([]*authtypes.Resource, 0) + for _, typeable := range typeables { + resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()}) + } + + return resources +} + +func (setter *setter) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) { + storableRole, err := setter.store.Get(ctx, orgID, id) + if err != nil { + return nil, err + } + + objects := make([]*authtypes.Object, 0) + for _, resource := range setter.GetResources(ctx) { + if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) { + resourceObjects, err := setter. + authz. + ListObjects( + ctx, + authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee), + relation, + authtypes.MustNewTypeableFromType(resource.Type, resource.Name), + ) + if err != nil { + return nil, err + } + + objects = append(objects, resourceObjects...) + } + } + + return objects, nil +} + +func (setter *setter) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error { + _, err := setter.licensing.GetActive(ctx, orgID) + if err != nil { + return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error()) + } + + return setter.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role)) +} + +func (setter *setter) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error { + _, err := setter.licensing.GetActive(ctx, orgID) + if err != nil { + return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error()) + } + + additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions) + if err != nil { + return err + } + + deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions) + if err != nil { + return err + } + + err = setter.authz.Write(ctx, additionTuples, deletionTuples) + if err != nil { + return err + } + + return nil +} + +func (setter *setter) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error { + _, err := setter.licensing.GetActive(ctx, orgID) + if err != nil { + return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error()) + } + + storableRole, err := setter.store.Get(ctx, orgID, id) + if err != nil { + return err + } + + role := roletypes.NewRoleFromStorableRole(storableRole) + err = role.CanEditDelete() + if err != nil { + return err + } + + return setter.store.Delete(ctx, orgID, id) +} + +func (setter *setter) MustGetTypeables() []authtypes.Typeable { + return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles} +} diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index f89e56632f..178857f7e6 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -211,7 +211,7 @@ func (s Server) HealthCheckStatus() chan healthcheck.Status { func (s *Server) createPublicServer(apiHandler *api.APIHandler, web web.Web) (*http.Server, error) { r := baseapp.NewRouter() - am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) + am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz, s.signoz.Modules.RoleGetter) r.Use(otelmux.Middleware( "apiserver", diff --git a/pkg/apiserver/signozapiserver/dashboard.go b/pkg/apiserver/signozapiserver/dashboard.go index 4fecb76a99..1b7f85952c 100644 --- a/pkg/apiserver/signozapiserver/dashboard.go +++ b/pkg/apiserver/signozapiserver/dashboard.go @@ -17,7 +17,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { ID: "CreatePublicDashboard", Tags: []string{"dashboard"}, Summary: "Create public dashboard", - Description: "This endpoints creates public sharing config and enables public sharing of the dashboard", + Description: "This endpoint creates public sharing config and enables public sharing of the dashboard", Request: new(dashboardtypes.PostablePublicDashboard), RequestContentType: "", Response: new(types.Identifiable), @@ -34,7 +34,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { ID: "GetPublicDashboard", Tags: []string{"dashboard"}, Summary: "Get public dashboard", - Description: "This endpoints returns public sharing config for a dashboard", + Description: "This endpoint returns public sharing config for a dashboard", Request: nil, RequestContentType: "", Response: new(dashboardtypes.GettablePublicDasbhboard), @@ -51,7 +51,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { ID: "UpdatePublicDashboard", Tags: []string{"dashboard"}, Summary: "Update public dashboard", - Description: "This endpoints updates the public sharing config for a dashboard", + Description: "This endpoint updates the public sharing config for a dashboard", Request: new(dashboardtypes.UpdatablePublicDashboard), RequestContentType: "", Response: nil, @@ -68,7 +68,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { ID: "DeletePublicDashboard", Tags: []string{"dashboard"}, Summary: "Delete public dashboard", - Description: "This endpoints deletes the public sharing config and disables the public sharing of a dashboard", + Description: "This endpoint deletes the public sharing config and disables the public sharing of a dashboard", Request: nil, RequestContentType: "", Response: nil, @@ -83,7 +83,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { if err := router.Handle("/api/v1/public/dashboards/{id}", handler.New(provider.authZ.CheckWithoutClaims( provider.dashboardHandler.GetPublicData, - authtypes.RelationRead, authtypes.RelationRead, + authtypes.RelationRead, dashboardtypes.TypeableMetaResourcePublicDashboard, func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) { id, err := valuer.NewUUID(mux.Vars(req)["id"]) @@ -92,11 +92,11 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { } return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs) - }), handler.OpenAPIDef{ + }, []string{}), handler.OpenAPIDef{ ID: "GetPublicDashboardData", Tags: []string{"dashboard"}, Summary: "Get public dashboard data", - Description: "This endpoints returns the sanitized dashboard data for public access", + Description: "This endpoint returns the sanitized dashboard data for public access", Request: nil, RequestContentType: "", Response: new(dashboardtypes.GettablePublicDashboardData), @@ -111,7 +111,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { if err := router.Handle("/api/v1/public/dashboards/{id}/widgets/{idx}/query_range", handler.New(provider.authZ.CheckWithoutClaims( provider.dashboardHandler.GetPublicWidgetQueryRange, - authtypes.RelationRead, authtypes.RelationRead, + authtypes.RelationRead, dashboardtypes.TypeableMetaResourcePublicDashboard, func(req *http.Request, orgs []*types.Organization) ([]authtypes.Selector, valuer.UUID, error) { id, err := valuer.NewUUID(mux.Vars(req)["id"]) @@ -120,7 +120,7 @@ func (provider *provider) addDashboardRoutes(router *mux.Router) error { } return provider.dashboardModule.GetPublicDashboardSelectorsAndOrg(req.Context(), id, orgs) - }), handler.OpenAPIDef{ + }, []string{}), handler.OpenAPIDef{ ID: "GetPublicDashboardWidgetQueryRange", Tags: []string{"dashboard"}, Summary: "Get query range result", diff --git a/pkg/apiserver/signozapiserver/global.go b/pkg/apiserver/signozapiserver/global.go index 0de19a853d..23db345212 100644 --- a/pkg/apiserver/signozapiserver/global.go +++ b/pkg/apiserver/signozapiserver/global.go @@ -13,7 +13,7 @@ func (provider *provider) addGlobalRoutes(router *mux.Router) error { ID: "GetGlobalConfig", Tags: []string{"global"}, Summary: "Get global config", - Description: "This endpoints returns global config", + Description: "This endpoint returns global config", Request: nil, RequestContentType: "", Response: new(types.GettableGlobalConfig), diff --git a/pkg/apiserver/signozapiserver/provider.go b/pkg/apiserver/signozapiserver/provider.go index 139361add7..8df4c41741 100644 --- a/pkg/apiserver/signozapiserver/provider.go +++ b/pkg/apiserver/signozapiserver/provider.go @@ -17,6 +17,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/modules/promote" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/types" @@ -41,6 +42,8 @@ type provider struct { dashboardHandler dashboard.Handler metricsExplorerHandler metricsexplorer.Handler gatewayHandler gateway.Handler + roleGetter role.Getter + roleHandler role.Handler } func NewFactory( @@ -58,9 +61,11 @@ func NewFactory( dashboardHandler dashboard.Handler, metricsExplorerHandler metricsexplorer.Handler, gatewayHandler gateway.Handler, + roleGetter role.Getter, + roleHandler role.Handler, ) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] { return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) { - return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler, gatewayHandler) + return newProvider(ctx, providerSettings, config, orgGetter, authz, orgHandler, userHandler, sessionHandler, authDomainHandler, preferenceHandler, globalHandler, promoteHandler, flaggerHandler, dashboardModule, dashboardHandler, metricsExplorerHandler, gatewayHandler, roleGetter, roleHandler) }) } @@ -82,6 +87,8 @@ func newProvider( dashboardHandler dashboard.Handler, metricsExplorerHandler metricsexplorer.Handler, gatewayHandler gateway.Handler, + roleGetter role.Getter, + roleHandler role.Handler, ) (apiserver.APIServer, error) { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver") router := mux.NewRouter().UseEncodedPath() @@ -102,9 +109,11 @@ func newProvider( dashboardHandler: dashboardHandler, metricsExplorerHandler: metricsExplorerHandler, gatewayHandler: gatewayHandler, + roleGetter: roleGetter, + roleHandler: roleHandler, } - provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz) + provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz, roleGetter) if err := provider.AddToRouter(router); err != nil { return nil, err @@ -162,6 +171,10 @@ func (provider *provider) AddToRouter(router *mux.Router) error { return err } + if err := provider.addRoleRoutes(router); err != nil { + return err + } + return nil } diff --git a/pkg/apiserver/signozapiserver/role.go b/pkg/apiserver/signozapiserver/role.go new file mode 100644 index 0000000000..13fc726a52 --- /dev/null +++ b/pkg/apiserver/signozapiserver/role.go @@ -0,0 +1,99 @@ +package signozapiserver + +import ( + "net/http" + + "github.com/SigNoz/signoz/pkg/http/handler" + "github.com/SigNoz/signoz/pkg/types" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/gorilla/mux" +) + +func (provider *provider) addRoleRoutes(router *mux.Router) error { + if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Create), handler.OpenAPIDef{ + ID: "CreateRole", + Tags: []string{"role"}, + Summary: "Create role", + Description: "This endpoint creates a role", + Request: nil, + RequestContentType: "", + Response: new(types.Identifiable), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusCreated, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleAdmin), + })).Methods(http.MethodPost).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v1/roles", handler.New(provider.authZ.AdminAccess(provider.roleHandler.List), handler.OpenAPIDef{ + ID: "ListRoles", + Tags: []string{"role"}, + Summary: "List roles", + Description: "This endpoint lists all roles", + Request: nil, + RequestContentType: "", + Response: make([]*roletypes.Role, 0), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleAdmin), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Get), handler.OpenAPIDef{ + ID: "GetRole", + Tags: []string{"role"}, + Summary: "Get role", + Description: "This endpoint gets a role", + Request: nil, + RequestContentType: "", + Response: new(roletypes.Role), + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusOK, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleAdmin), + })).Methods(http.MethodGet).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Patch), handler.OpenAPIDef{ + ID: "PatchRole", + Tags: []string{"role"}, + Summary: "Patch role", + Description: "This endpoint patches a role", + Request: nil, + RequestContentType: "", + Response: nil, + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusNoContent, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleAdmin), + })).Methods(http.MethodPatch).GetError(); err != nil { + return err + } + + if err := router.Handle("/api/v1/roles/{id}", handler.New(provider.authZ.AdminAccess(provider.roleHandler.Delete), handler.OpenAPIDef{ + ID: "DeleteRole", + Tags: []string{"role"}, + Summary: "Delete role", + Description: "This endpoint deletes a role", + Request: nil, + RequestContentType: "", + Response: nil, + ResponseContentType: "application/json", + SuccessStatusCode: http.StatusNoContent, + ErrorStatusCodes: []int{}, + Deprecated: false, + SecuritySchemes: newSecuritySchemes(types.RoleAdmin), + })).Methods(http.MethodDelete).GetError(); err != nil { + return err + } + + return nil +} diff --git a/pkg/authz/authz.go b/pkg/authz/authz.go index 87c8bbc475..1d0e636959 100644 --- a/pkg/authz/authz.go +++ b/pkg/authz/authz.go @@ -16,9 +16,10 @@ type AuthZ interface { Check(context.Context, *openfgav1.TupleKey) error // CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does. - CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error + CheckWithTupleCreation(context.Context, authtypes.Claims, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error - CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error + // CheckWithTupleCreationWithoutClaims checks permissions for anonymous users. + CheckWithTupleCreationWithoutClaims(context.Context, valuer.UUID, authtypes.Relation, authtypes.Typeable, []authtypes.Selector, []authtypes.Selector) error // Batch Check returns error when the upstream authorization server is unavailable or for all the tuples of subject (s) doesn't have relation (r) on object (o). BatchCheck(context.Context, []*openfgav1.TupleKey) error diff --git a/pkg/authz/openfgaauthz/provider.go b/pkg/authz/openfgaauthz/provider.go index cb300305dc..bb531679b5 100644 --- a/pkg/authz/openfgaauthz/provider.go +++ b/pkg/authz/openfgaauthz/provider.go @@ -152,17 +152,17 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1. } } - return errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "") + return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "none of the subjects are allowed for requested access") } -func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error { +func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error { subject, err := authtypes.NewSubject(authtypes.TypeableUser, claims.UserID, orgID, nil) if err != nil { return err } - tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID) + tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID) if err != nil { return err } @@ -175,13 +175,13 @@ func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims aut return nil } -func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error { +func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Context, orgID valuer.UUID, _ authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector, roleSelectors []authtypes.Selector) error { subject, err := authtypes.NewSubject(authtypes.TypeableAnonymous, authtypes.AnonymousUser.String(), orgID, nil) if err != nil { return err } - tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID) + tuples, err := authtypes.TypeableRole.Tuples(subject, authtypes.RelationAssignee, roleSelectors, orgID) if err != nil { return err } @@ -195,6 +195,10 @@ func (provider *provider) CheckWithTupleCreationWithoutClaims(ctx context.Contex } func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error { + if len(additions) == 0 && len(deletions) == 0 { + return nil + } + storeID, modelID := provider.getStoreIDandModelID() deletionTuplesWithoutCondition := make([]*openfgav1.TupleKeyWithoutCondition, len(deletions)) for idx, tuple := range deletions { diff --git a/pkg/authz/openfgaauthz/provider_test.go b/pkg/authz/openfgaauthz/provider_test.go index 47345d1bba..d32d7ee6ae 100644 --- a/pkg/authz/openfgaauthz/provider_test.go +++ b/pkg/authz/openfgaauthz/provider_test.go @@ -34,11 +34,11 @@ func TestProviderStartStop(t *testing.T) { sqlstore.Mock().ExpectQuery("SELECT authorization_model_id, schema_version, type, type_definition, serialized_protobuf FROM authorization_model WHERE authorization_model_id = (.+) AND store = (.+)").WithArgs("01K44QQKXR6F729W160NFCJT58", "01K3V0NTN47MPTMEV1PD5ST6ZC").WillReturnRows(modelRows) sqlstore.Mock().ExpectExec("INSERT INTO authorization_model (.+) VALUES (.+)").WillReturnResult(sqlmock.NewResult(1, 1)) + go func() { err := provider.Start(context.Background()) require.NoError(t, err) }() - // wait for the service to start time.Sleep(time.Second * 2) diff --git a/pkg/http/middleware/authz.go b/pkg/http/middleware/authz.go index 164df1f38c..5081aaf551 100644 --- a/pkg/http/middleware/authz.go +++ b/pkg/http/middleware/authz.go @@ -7,6 +7,7 @@ import ( "github.com/SigNoz/signoz/pkg/authz" "github.com/SigNoz/signoz/pkg/http/render" "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/valuer" "github.com/gorilla/mux" @@ -20,14 +21,15 @@ type AuthZ struct { logger *slog.Logger orgGetter organization.Getter authzService authz.AuthZ + roleGetter role.Getter } -func NewAuthZ(logger *slog.Logger, orgGetter organization.Getter, authzService authz.AuthZ) *AuthZ { +func NewAuthZ(logger *slog.Logger, orgGetter organization.Getter, authzService authz.AuthZ, roleGetter role.Getter) *AuthZ { if logger == nil { panic("cannot build authz middleware, logger is empty") } - return &AuthZ{logger: logger, orgGetter: orgGetter, authzService: authzService} + return &AuthZ{logger: logger, orgGetter: orgGetter, authzService: authzService, roleGetter: roleGetter} } func (middleware *AuthZ) ViewAccess(next http.HandlerFunc) http.HandlerFunc { @@ -109,9 +111,10 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc { }) } -func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithClaimsFn) http.HandlerFunc { +func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithClaimsFn, roles []string) http.HandlerFunc { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - claims, err := authtypes.ClaimsFromContext(req.Context()) + ctx := req.Context() + claims, err := authtypes.ClaimsFromContext(ctx) if err != nil { render.Error(rw, err) return @@ -129,7 +132,18 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio return } - err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, orgId, relation, translation, typeable, selectors) + roles, err := middleware.roleGetter.ListByOrgIDAndNames(req.Context(), orgId, roles) + if err != nil { + render.Error(rw, err) + return + } + + roleSelectors := []authtypes.Selector{} + for _, role := range roles { + selectors = append(selectors, authtypes.MustNewSelector(authtypes.TypeRole, role.ID.String())) + } + + err = middleware.authzService.CheckWithTupleCreation(ctx, claims, orgId, relation, typeable, selectors, roleSelectors) if err != nil { render.Error(rw, err) return @@ -139,7 +153,7 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio }) } -func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithoutClaimsFn) http.HandlerFunc { +func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackWithoutClaimsFn, roles []string) http.HandlerFunc { return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { ctx := req.Context() orgs, err := middleware.orgGetter.ListByOwnedKeyRange(ctx) @@ -154,7 +168,7 @@ func (middleware *AuthZ) CheckWithoutClaims(next http.HandlerFunc, relation auth return } - err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, translation, typeable, selectors) + err = middleware.authzService.CheckWithTupleCreationWithoutClaims(ctx, orgID, relation, typeable, selectors, selectors) if err != nil { render.Error(rw, err) return diff --git a/pkg/modules/role/implrole/getter.go b/pkg/modules/role/implrole/getter.go new file mode 100644 index 0000000000..b7dade1813 --- /dev/null +++ b/pkg/modules/role/implrole/getter.go @@ -0,0 +1,63 @@ +package implrole + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type getter struct { + store roletypes.Store +} + +func NewGetter(store roletypes.Store) role.Getter { + return &getter{store: store} +} + +func (getter *getter) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) { + storableRole, err := getter.store.Get(ctx, orgID, id) + if err != nil { + return nil, err + } + + return roletypes.NewRoleFromStorableRole(storableRole), nil +} + +func (getter *getter) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) { + storableRole, err := getter.store.GetByOrgIDAndName(ctx, orgID, name) + if err != nil { + return nil, err + } + + return roletypes.NewRoleFromStorableRole(storableRole), nil +} + +func (getter *getter) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) { + storableRoles, err := getter.store.List(ctx, orgID) + if err != nil { + return nil, err + } + + roles := make([]*roletypes.Role, len(storableRoles)) + for idx, storableRole := range storableRoles { + roles[idx] = roletypes.NewRoleFromStorableRole(storableRole) + } + + return roles, nil +} + +func (getter *getter) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) { + storableRoles, err := getter.store.ListByOrgIDAndNames(ctx, orgID, names) + if err != nil { + return nil, err + } + + roles := make([]*roletypes.Role, len(storableRoles)) + for idx, storable := range storableRoles { + roles[idx] = roletypes.NewRoleFromStorableRole(storable) + } + + return roles, nil +} diff --git a/pkg/modules/role/implrole/granter.go b/pkg/modules/role/implrole/granter.go new file mode 100644 index 0000000000..268894510d --- /dev/null +++ b/pkg/modules/role/implrole/granter.go @@ -0,0 +1,108 @@ +package implrole + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/authz" + "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type granter struct { + store roletypes.Store + authz authz.AuthZ +} + +func NewGranter(store roletypes.Store, authz authz.AuthZ) role.Granter { + return &granter{store: store, authz: authz} +} + +func (granter *granter) Grant(ctx context.Context, orgID valuer.UUID, name string, subject string) error { + role, err := granter.store.GetByOrgIDAndName(ctx, orgID, name) + if err != nil { + return err + } + + tuples, err := authtypes.TypeableRole.Tuples( + subject, + authtypes.RelationAssignee, + []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeRole, role.ID.StringValue()), + }, + orgID, + ) + if err != nil { + return err + } + return granter.authz.Write(ctx, tuples, nil) +} + +func (granter *granter) GrantByID(ctx context.Context, orgID valuer.UUID, id valuer.UUID, subject string) error { + tuples, err := authtypes.TypeableRole.Tuples( + subject, + authtypes.RelationAssignee, + []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeRole, id.StringValue()), + }, + orgID, + ) + if err != nil { + return err + } + return granter.authz.Write(ctx, tuples, nil) +} + +func (granter *granter) ModifyGrant(ctx context.Context, orgID valuer.UUID, existingRoleName string, updatedRoleName string, subject string) error { + err := granter.Revoke(ctx, orgID, existingRoleName, subject) + if err != nil { + return err + } + + err = granter.Grant(ctx, orgID, updatedRoleName, subject) + if err != nil { + return err + } + + return nil +} + +func (granter *granter) Revoke(ctx context.Context, orgID valuer.UUID, name string, subject string) error { + role, err := granter.store.GetByOrgIDAndName(ctx, orgID, name) + if err != nil { + return err + } + + tuples, err := authtypes.TypeableRole.Tuples( + subject, + authtypes.RelationAssignee, + []authtypes.Selector{ + authtypes.MustNewSelector(authtypes.TypeRole, role.ID.StringValue()), + }, + orgID, + ) + if err != nil { + return err + } + return granter.authz.Write(ctx, nil, tuples) +} + +func (granter *granter) CreateManagedRoles(ctx context.Context, _ valuer.UUID, managedRoles []*roletypes.Role) error { + err := granter.store.RunInTx(ctx, func(ctx context.Context) error { + for _, role := range managedRoles { + err := granter.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return err + } + + return nil +} diff --git a/pkg/modules/role/implrole/handler.go b/pkg/modules/role/implrole/handler.go index 38cea6241c..be383b57fa 100644 --- a/pkg/modules/role/implrole/handler.go +++ b/pkg/modules/role/implrole/handler.go @@ -14,11 +14,12 @@ import ( ) type handler struct { - module role.Module + setter role.Setter + getter role.Getter } -func NewHandler(module role.Module) role.Handler { - return &handler{module: module} +func NewHandler(setter role.Setter, getter role.Getter) role.Handler { + return &handler{setter: setter, getter: getter} } func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { @@ -35,7 +36,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) { return } - err = handler.module.Create(ctx, roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom.StringValue(), valuer.MustNewUUID(claims.OrgID))) + err = handler.setter.Create(ctx, valuer.MustNewUUID(claims.OrgID), roletypes.NewRole(req.Name, req.Description, roletypes.RoleTypeCustom, valuer.MustNewUUID(claims.OrgID))) if err != nil { render.Error(rw, err) return @@ -63,7 +64,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) { return } - role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID) + role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID) if err != nil { render.Error(rw, err) return @@ -102,7 +103,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) { return } - objects, err := handler.module.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation) + objects, err := handler.setter.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation) if err != nil { render.Error(rw, err) return @@ -113,7 +114,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) { func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - resources := handler.module.GetResources(ctx) + resources := handler.setter.GetResources(ctx) var resourceRelations = struct { Resources []*authtypes.Resource `json:"resources"` @@ -133,7 +134,7 @@ func (handler *handler) List(rw http.ResponseWriter, r *http.Request) { return } - roles, err := handler.module.List(ctx, valuer.MustNewUUID(claims.OrgID)) + roles, err := handler.getter.List(ctx, valuer.MustNewUUID(claims.OrgID)) if err != nil { render.Error(rw, err) return @@ -162,14 +163,19 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) { return } - role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) + role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(rw, err) return } - role.PatchMetadata(req.Name, req.Description) - err = handler.module.Patch(ctx, valuer.MustNewUUID(claims.OrgID), role) + err = role.PatchMetadata(req.Name, req.Description) + if err != nil { + render.Error(rw, err) + return + } + + err = handler.setter.Patch(ctx, valuer.MustNewUUID(claims.OrgID), role) if err != nil { render.Error(rw, err) return @@ -204,13 +210,19 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) { return } - patchableObjects, err := roletypes.NewPatchableObjects(req.Additions, req.Deletions, relation) + role, err := handler.getter.Get(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(rw, err) return } - err = handler.module.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), id, relation, patchableObjects.Additions, patchableObjects.Deletions) + patchableObjects, err := role.NewPatchableObjects(req.Additions, req.Deletions, relation) + if err != nil { + render.Error(rw, err) + return + } + + err = handler.setter.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), id, relation, patchableObjects.Additions, patchableObjects.Deletions) if err != nil { render.Error(rw, err) return @@ -233,7 +245,7 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) { return } - err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id) + err = handler.setter.Delete(ctx, valuer.MustNewUUID(claims.OrgID), id) if err != nil { render.Error(rw, err) return diff --git a/pkg/modules/role/implrole/module.go b/pkg/modules/role/implrole/module.go deleted file mode 100644 index e100269d5a..0000000000 --- a/pkg/modules/role/implrole/module.go +++ /dev/null @@ -1,164 +0,0 @@ -package implrole - -import ( - "context" - "slices" - - "github.com/SigNoz/signoz/pkg/authz" - "github.com/SigNoz/signoz/pkg/errors" - "github.com/SigNoz/signoz/pkg/modules/role" - "github.com/SigNoz/signoz/pkg/types/authtypes" - "github.com/SigNoz/signoz/pkg/types/roletypes" - "github.com/SigNoz/signoz/pkg/valuer" -) - -type module struct { - store roletypes.Store - registry []role.RegisterTypeable - authz authz.AuthZ -} - -func NewModule(store roletypes.Store, authz authz.AuthZ, registry []role.RegisterTypeable) role.Module { - return &module{ - store: store, - authz: authz, - registry: registry, - } -} - -func (module *module) Create(ctx context.Context, role *roletypes.Role) error { - return module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) -} - -func (module *module) GetOrCreate(ctx context.Context, role *roletypes.Role) (*roletypes.Role, error) { - existingRole, err := module.store.GetByNameAndOrgID(ctx, role.Name, role.OrgID) - if err != nil { - if !errors.Ast(err, errors.TypeNotFound) { - return nil, err - } - } - - if existingRole != nil { - return roletypes.NewRoleFromStorableRole(existingRole), nil - } - - err = module.store.Create(ctx, roletypes.NewStorableRoleFromRole(role)) - if err != nil { - return nil, err - } - - return role, nil -} - -func (module *module) GetResources(_ context.Context) []*authtypes.Resource { - typeables := make([]authtypes.Typeable, 0) - for _, register := range module.registry { - typeables = append(typeables, register.MustGetTypeables()...) - } - // role module cannot self register itself! - typeables = append(typeables, module.MustGetTypeables()...) - - resources := make([]*authtypes.Resource, 0) - for _, typeable := range typeables { - resources = append(resources, &authtypes.Resource{Name: typeable.Name(), Type: typeable.Type()}) - } - - return resources -} - -func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) { - storableRole, err := module.store.Get(ctx, orgID, id) - if err != nil { - return nil, err - } - - return roletypes.NewRoleFromStorableRole(storableRole), nil -} - -func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) { - storableRole, err := module.store.Get(ctx, orgID, id) - if err != nil { - return nil, err - } - - objects := make([]*authtypes.Object, 0) - for _, resource := range module.GetResources(ctx) { - if slices.Contains(authtypes.TypeableRelations[resource.Type], relation) { - resourceObjects, err := module. - authz. - ListObjects( - ctx, - authtypes.MustNewSubject(authtypes.TypeableRole, storableRole.ID.String(), orgID, &authtypes.RelationAssignee), - relation, - authtypes.MustNewTypeableFromType(resource.Type, resource.Name), - ) - if err != nil { - return nil, err - } - - objects = append(objects, resourceObjects...) - } - } - - return objects, nil -} - -func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) { - storableRoles, err := module.store.List(ctx, orgID) - if err != nil { - return nil, err - } - - roles := make([]*roletypes.Role, len(storableRoles)) - for idx, storableRole := range storableRoles { - roles[idx] = roletypes.NewRoleFromStorableRole(storableRole) - } - - return roles, nil -} - -func (module *module) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error { - return module.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role)) -} - -func (module *module) PatchObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation, additions, deletions []*authtypes.Object) error { - additionTuples, err := roletypes.GetAdditionTuples(id, orgID, relation, additions) - if err != nil { - return err - } - - deletionTuples, err := roletypes.GetDeletionTuples(id, orgID, relation, deletions) - if err != nil { - return err - } - - err = module.authz.Write(ctx, additionTuples, deletionTuples) - if err != nil { - return err - } - - return nil -} - -func (module *module) Assign(ctx context.Context, id valuer.UUID, orgID valuer.UUID, subject string) error { - tuples, err := authtypes.TypeableRole.Tuples( - subject, - authtypes.RelationAssignee, - []authtypes.Selector{ - authtypes.MustNewSelector(authtypes.TypeRole, id.StringValue()), - }, - orgID, - ) - if err != nil { - return err - } - return module.authz.Write(ctx, tuples, nil) -} - -func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error { - return module.store.Delete(ctx, orgID, id) -} - -func (module *module) MustGetTypeables() []authtypes.Typeable { - return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles} -} diff --git a/pkg/modules/role/implrole/setter.go b/pkg/modules/role/implrole/setter.go new file mode 100644 index 0000000000..0eb6f2f4ab --- /dev/null +++ b/pkg/modules/role/implrole/setter.go @@ -0,0 +1,53 @@ +package implrole + +import ( + "context" + + "github.com/SigNoz/signoz/pkg/authz" + "github.com/SigNoz/signoz/pkg/errors" + "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/types/authtypes" + "github.com/SigNoz/signoz/pkg/types/roletypes" + "github.com/SigNoz/signoz/pkg/valuer" +) + +type setter struct { + store roletypes.Store + authz authz.AuthZ +} + +func NewSetter(store roletypes.Store, authz authz.AuthZ) role.Setter { + return &setter{store: store, authz: authz} +} + +func (setter *setter) Create(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error { + return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) GetOrCreate(_ context.Context, _ valuer.UUID, _ *roletypes.Role) (*roletypes.Role, error) { + return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) GetResources(_ context.Context) []*authtypes.Resource { + return nil +} + +func (setter *setter) GetObjects(ctx context.Context, orgID valuer.UUID, id valuer.UUID, relation authtypes.Relation) ([]*authtypes.Object, error) { + return nil, errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) Patch(_ context.Context, _ valuer.UUID, _ *roletypes.Role) error { + return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) PatchObjects(_ context.Context, _ valuer.UUID, _ valuer.UUID, _ authtypes.Relation, _, _ []*authtypes.Object) error { + return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) Delete(_ context.Context, _ valuer.UUID, _ valuer.UUID) error { + return errors.Newf(errors.TypeUnsupported, roletypes.ErrCodeRoleUnsupported, "not implemented") +} + +func (setter *setter) MustGetTypeables() []authtypes.Typeable { + return nil +} diff --git a/pkg/modules/role/implrole/store.go b/pkg/modules/role/implrole/store.go index f704617dc3..51906bbf79 100644 --- a/pkg/modules/role/implrole/store.go +++ b/pkg/modules/role/implrole/store.go @@ -7,6 +7,7 @@ import ( "github.com/SigNoz/signoz/pkg/sqlstore" "github.com/SigNoz/signoz/pkg/types/roletypes" "github.com/SigNoz/signoz/pkg/valuer" + "github.com/uptrace/bun" ) type store struct { @@ -20,7 +21,7 @@ func NewStore(sqlstore sqlstore.SQLStore) roletypes.Store { func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error { _, err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewInsert(). Model(role). Exec(ctx) @@ -35,7 +36,7 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) role := new(roletypes.StorableRole) err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewSelect(). Model(role). Where("org_id = ?", orgID). @@ -48,11 +49,11 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) return role, nil } -func (store *store) GetByNameAndOrgID(ctx context.Context, name string, orgID valuer.UUID) (*roletypes.StorableRole, error) { +func (store *store) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.StorableRole, error) { role := new(roletypes.StorableRole) err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewSelect(). Model(role). Where("org_id = ?", orgID). @@ -69,13 +70,30 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S roles := make([]*roletypes.StorableRole, 0) err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewSelect(). Model(&roles). Where("org_id = ?", orgID). Scan(ctx) if err != nil { - return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "no roles found in org_id: %s", orgID) + return nil, err + } + + return roles, nil +} + +func (store *store) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.StorableRole, error) { + roles := make([]*roletypes.StorableRole, 0) + err := store. + sqlstore. + BunDBCtx(ctx). + NewSelect(). + Model(&roles). + Where("org_id = ?", orgID). + Where("name IN (?)", bun.In(names)). + Scan(ctx) + if err != nil { + return nil, err } return roles, nil @@ -84,7 +102,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error { _, err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewUpdate(). Model(role). WherePK(). @@ -100,7 +118,7 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletyp func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error { _, err := store. sqlstore. - BunDB(). + BunDBCtx(ctx). NewDelete(). Model(new(roletypes.StorableRole)). Where("org_id = ?", orgID). diff --git a/pkg/modules/role/role.go b/pkg/modules/role/role.go index 87ec4c501f..e3d3bf4907 100644 --- a/pkg/modules/role/role.go +++ b/pkg/modules/role/role.go @@ -9,22 +9,16 @@ import ( "github.com/SigNoz/signoz/pkg/valuer" ) -type Module interface { +type Setter interface { // Creates the role. - Create(context.Context, *roletypes.Role) error + Create(context.Context, valuer.UUID, *roletypes.Role) error // Gets the role if it exists or creates one. - GetOrCreate(context.Context, *roletypes.Role) (*roletypes.Role, error) - - // Gets the role - Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error) + GetOrCreate(context.Context, valuer.UUID, *roletypes.Role) (*roletypes.Role, error) // Gets the objects associated with the given role and relation. GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error) - // Lists all the roles for the organization. - List(context.Context, valuer.UUID) ([]*roletypes.Role, error) - // Gets all the typeable resources registered from role registry. GetResources(context.Context) []*authtypes.Resource @@ -37,12 +31,40 @@ type Module interface { // Deletes the role and tuples in authorization server. Delete(context.Context, valuer.UUID, valuer.UUID) error - // Assigns role to the given subject. - Assign(context.Context, valuer.UUID, valuer.UUID, string) error - RegisterTypeable } +type Getter interface { + // Gets the role + Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error) + + // Gets the role by org_id and name + GetByOrgIDAndName(context.Context, valuer.UUID, string) (*roletypes.Role, error) + + // Lists all the roles for the organization. + List(context.Context, valuer.UUID) ([]*roletypes.Role, error) + + // Lists all the roles for the organization filtered by name + ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*roletypes.Role, error) +} + +type Granter interface { + // Grants a role to the subject based on role name. + Grant(context.Context, valuer.UUID, string, string) error + + // Grants a role to the subject based on role id. + GrantByID(context.Context, valuer.UUID, valuer.UUID, string) error + + // Revokes a granted role from the subject based on role name. + Revoke(context.Context, valuer.UUID, string, string) error + + // Changes the granted role for the subject based on role name. + ModifyGrant(context.Context, valuer.UUID, string, string, string) error + + // Bootstrap the managed roles. + CreateManagedRoles(context.Context, valuer.UUID, []*roletypes.Role) error +} + type RegisterTypeable interface { MustGetTypeables() []authtypes.Typeable } diff --git a/pkg/modules/user/impluser/module.go b/pkg/modules/user/impluser/module.go index d75e5c395c..aca71d8db7 100644 --- a/pkg/modules/user/impluser/module.go +++ b/pkg/modules/user/impluser/module.go @@ -12,6 +12,7 @@ import ( "github.com/SigNoz/signoz/pkg/errors" "github.com/SigNoz/signoz/pkg/factory" "github.com/SigNoz/signoz/pkg/modules/organization" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/modules/user" root "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/tokenizer" @@ -29,12 +30,13 @@ type Module struct { emailing emailing.Emailing settings factory.ScopedProviderSettings orgSetter organization.Setter + granter role.Granter analytics analytics.Analytics config user.Config } // This module is a WIP, don't take inspiration from this. -func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, analytics analytics.Analytics, config user.Config) root.Module { +func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing emailing.Emailing, providerSettings factory.ProviderSettings, orgSetter organization.Setter, granter role.Granter, analytics analytics.Analytics, config user.Config) root.Module { settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/modules/user/impluser") return &Module{ store: store, @@ -43,6 +45,7 @@ func NewModule(store types.UserStore, tokenizer tokenizer.Tokenizer, emailing em settings: settings, orgSetter: orgSetter, analytics: analytics, + granter: granter, config: config, } } @@ -227,7 +230,6 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u } user.UpdatedAt = time.Now() - updatedUser, err := m.store.UpdateUser(ctx, orgID, id, user) if err != nil { return nil, err @@ -258,8 +260,8 @@ func (m *Module) UpdateUser(ctx context.Context, orgID valuer.UUID, id string, u return updatedUser, nil } -func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error { - user, err := m.store.GetUser(ctx, valuer.MustNewUUID(id)) +func (module *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, deletedBy string) error { + user, err := module.store.GetUser(ctx, valuer.MustNewUUID(id)) if err != nil { return err } @@ -269,7 +271,7 @@ func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, d } // don't allow to delete the last admin user - adminUsers, err := m.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID) + adminUsers, err := module.store.GetUsersByRoleAndOrgID(ctx, types.RoleAdmin, orgID) if err != nil { return err } @@ -278,11 +280,11 @@ func (m *Module) DeleteUser(ctx context.Context, orgID valuer.UUID, id string, d return errors.New(errors.TypeForbidden, errors.CodeForbidden, "cannot delete the last admin") } - if err := m.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil { + if err := module.store.DeleteUser(ctx, orgID.String(), user.ID.StringValue()); err != nil { return err } - m.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{ + module.analytics.TrackUser(ctx, user.OrgID.String(), user.ID.String(), "User Deleted", map[string]any{ "deleted_by": deletedBy, }) @@ -476,7 +478,7 @@ func (module *Module) CreateFirstUser(ctx context.Context, organization *types.O } if err = module.store.RunInTx(ctx, func(ctx context.Context) error { - err := module.orgSetter.Create(ctx, organization) + err = module.orgSetter.Create(ctx, organization) if err != nil { return err } diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index 5c049920b2..9a213678e9 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -209,7 +209,7 @@ func (s *Server) createPublicServer(api *APIHandler, web web.Web) (*http.Server, r.Use(middleware.NewLogging(s.signoz.Instrumentation.Logger(), s.config.APIServer.Logging.ExcludedRoutes).Wrap) r.Use(middleware.NewComment().Wrap) - am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz) + am := middleware.NewAuthZ(s.signoz.Instrumentation.Logger(), s.signoz.Modules.OrgGetter, s.signoz.Authz, s.signoz.Modules.RoleGetter) api.RegisterRoutes(r, am) api.RegisterLogsRoutes(r, am) diff --git a/pkg/signoz/handler.go b/pkg/signoz/handler.go index 50bd761475..eb771da2ac 100644 --- a/pkg/signoz/handler.go +++ b/pkg/signoz/handler.go @@ -17,6 +17,8 @@ import ( "github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport" + "github.com/SigNoz/signoz/pkg/modules/role" + "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/modules/savedview" "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/services" @@ -41,6 +43,7 @@ type Handlers struct { Global global.Handler FlaggerHandler flagger.Handler GatewayHandler gateway.Handler + Role role.Handler } func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, querier querier.Querier, licensing licensing.Licensing, global global.Global, flaggerService flagger.Flagger, gatewayService gateway.Gateway) Handlers { @@ -57,5 +60,6 @@ func NewHandlers(modules Modules, providerSettings factory.ProviderSettings, que Global: signozglobal.NewHandler(global), FlaggerHandler: flagger.NewHandler(flaggerService), GatewayHandler: gateway.NewHandler(gatewayService), + Role: implrole.NewHandler(modules.RoleSetter, modules.RoleGetter), } } diff --git a/pkg/signoz/handler_test.go b/pkg/signoz/handler_test.go index 021015e3fd..68eaceaf45 100644 --- a/pkg/signoz/handler_test.go +++ b/pkg/signoz/handler_test.go @@ -13,6 +13,7 @@ import ( "github.com/SigNoz/signoz/pkg/factory/factorytest" "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization" + "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/sharder" "github.com/SigNoz/signoz/pkg/sharder/noopsharder" @@ -40,7 +41,10 @@ func TestNewHandlers(t *testing.T) { queryParser := queryparser.New(providerSettings) require.NoError(t, err) dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule) + roleSetter := implrole.NewSetter(implrole.NewStore(sqlstore), nil) + roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore)) + grantModule := implrole.NewGranter(implrole.NewStore(sqlstore), nil) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, roleSetter, roleGetter, grantModule) handlers := NewHandlers(modules, providerSettings, nil, nil, nil, nil, nil) diff --git a/pkg/signoz/module.go b/pkg/signoz/module.go index 408d2f8d10..f71e9c3faf 100644 --- a/pkg/signoz/module.go +++ b/pkg/signoz/module.go @@ -25,6 +25,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/quickfilter/implquickfilter" "github.com/SigNoz/signoz/pkg/modules/rawdataexport" "github.com/SigNoz/signoz/pkg/modules/rawdataexport/implrawdataexport" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/modules/savedview" "github.com/SigNoz/signoz/pkg/modules/savedview/implsavedview" "github.com/SigNoz/signoz/pkg/modules/services" @@ -66,6 +67,9 @@ type Modules struct { SpanPercentile spanpercentile.Module MetricsExplorer metricsexplorer.Module Promote promote.Module + RoleSetter role.Setter + RoleGetter role.Getter + Granter role.Granter } func NewModules( @@ -85,10 +89,13 @@ func NewModules( queryParser queryparser.QueryParser, config Config, dashboard dashboard.Module, + roleSetter role.Setter, + roleGetter role.Getter, + granter role.Granter, ) Modules { quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore)) orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter) - user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics, config.User) + user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, granter, analytics, config.User) userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings)) ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings) @@ -110,5 +117,8 @@ func NewModules( Services: implservices.NewModule(querier, telemetryStore), MetricsExplorer: implmetricsexplorer.NewModule(telemetryStore, telemetryMetadataStore, cache, ruleStore, dashboard, providerSettings, config.MetricsExplorer), Promote: implpromote.NewModule(telemetryMetadataStore, telemetryStore), + RoleSetter: roleSetter, + RoleGetter: roleGetter, + Granter: granter, } } diff --git a/pkg/signoz/module_test.go b/pkg/signoz/module_test.go index 50e6e48932..aa718e141c 100644 --- a/pkg/signoz/module_test.go +++ b/pkg/signoz/module_test.go @@ -13,6 +13,7 @@ import ( "github.com/SigNoz/signoz/pkg/factory/factorytest" "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard" "github.com/SigNoz/signoz/pkg/modules/organization/implorganization" + "github.com/SigNoz/signoz/pkg/modules/role/implrole" "github.com/SigNoz/signoz/pkg/queryparser" "github.com/SigNoz/signoz/pkg/sharder" "github.com/SigNoz/signoz/pkg/sharder/noopsharder" @@ -40,7 +41,10 @@ func TestNewModules(t *testing.T) { queryParser := queryparser.New(providerSettings) require.NoError(t, err) dashboardModule := impldashboard.NewModule(impldashboard.NewStore(sqlstore), providerSettings, nil, orgGetter, queryParser) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule) + roleSetter := implrole.NewSetter(implrole.NewStore(sqlstore), nil) + roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore)) + grantModule := implrole.NewGranter(implrole.NewStore(sqlstore), nil) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil, nil, nil, nil, queryParser, Config{}, dashboardModule, roleSetter, roleGetter, grantModule) reflectVal := reflect.ValueOf(modules) for i := 0; i < reflectVal.NumField(); i++ { diff --git a/pkg/signoz/openapi.go b/pkg/signoz/openapi.go index 6a341f93d5..8381f1958d 100644 --- a/pkg/signoz/openapi.go +++ b/pkg/signoz/openapi.go @@ -19,6 +19,7 @@ import ( "github.com/SigNoz/signoz/pkg/modules/organization" "github.com/SigNoz/signoz/pkg/modules/preference" "github.com/SigNoz/signoz/pkg/modules/promote" + "github.com/SigNoz/signoz/pkg/modules/role" "github.com/SigNoz/signoz/pkg/modules/session" "github.com/SigNoz/signoz/pkg/modules/user" "github.com/SigNoz/signoz/pkg/types/ctxtypes" @@ -49,6 +50,8 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta struct{ dashboard.Handler }{}, struct{ metricsexplorer.Handler }{}, struct{ gateway.Handler }{}, + struct{ role.Getter }{}, + struct{ role.Handler }{}, ).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{}) if err != nil { return nil, err diff --git a/pkg/signoz/provider.go b/pkg/signoz/provider.go index 753fff70f5..3dadaef665 100644 --- a/pkg/signoz/provider.go +++ b/pkg/signoz/provider.go @@ -243,6 +243,8 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au handlers.Dashboard, handlers.MetricsExplorer, handlers.GatewayHandler, + modules.RoleGetter, + handlers.Role, ), ) } diff --git a/pkg/signoz/signoz.go b/pkg/signoz/signoz.go index b1badcfa2b..59bdd75397 100644 --- a/pkg/signoz/signoz.go +++ b/pkg/signoz/signoz.go @@ -90,8 +90,9 @@ func New( telemetrystoreProviderFactories factory.NamedMap[factory.ProviderFactory[telemetrystore.TelemetryStore, telemetrystore.Config]], authNsCallback func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error), authzCallback func(context.Context, sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config], - dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, role.Module, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module, + dashboardModuleCallback func(sqlstore.SQLStore, factory.ProviderSettings, analytics.Analytics, organization.Getter, role.Setter, role.Granter, queryparser.QueryParser, querier.Querier, licensing.Licensing) dashboard.Module, gatewayProviderFactory func(licensing.Licensing) factory.ProviderFactory[gateway.Gateway, gateway.Config], + roleSetterCallback func(sqlstore.SQLStore, authz.AuthZ, licensing.Licensing, []role.RegisterTypeable) role.Setter, ) (*SigNoz, error) { // Initialize instrumentation instrumentation, err := instrumentation.New(ctx, config.Instrumentation, version.Info, "signoz") @@ -280,6 +281,12 @@ func New( return nil, err } + // Initialize user getter + userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings)) + + // Initialize the role getter + roleGetter := implrole.NewGetter(implrole.NewStore(sqlstore)) + // Initialize authz authzProviderFactory := authzCallback(ctx, sqlstore) authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{}) @@ -287,9 +294,6 @@ func New( return nil, err } - // Initialize user getter - userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings)) - // Initialize notification manager from the available notification manager provider factories nfManager, err := factory.NewProviderFromNamedMap( ctx, @@ -386,9 +390,10 @@ func New( } // Initialize all modules - roleModule := implrole.NewModule(implrole.NewStore(sqlstore), authz, nil) - dashboardModule := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleModule, queryParser, querier, licensing) - modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboardModule) + roleSetter := roleSetterCallback(sqlstore, authz, licensing, nil) + granter := implrole.NewGranter(implrole.NewStore(sqlstore), authz) + dashboard := dashboardModuleCallback(sqlstore, providerSettings, analytics, orgGetter, roleSetter, granter, queryParser, querier, licensing) + modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, telemetrystore, telemetryMetadataStore, authNs, authz, cache, queryParser, config, dashboard, roleSetter, roleGetter, granter) // Initialize all handlers for the modules handlers := NewHandlers(modules, providerSettings, querier, licensing, global, flagger, gateway) diff --git a/pkg/types/authtypes/selector.go b/pkg/types/authtypes/selector.go index e9e13aceb2..c7d43d1ace 100644 --- a/pkg/types/authtypes/selector.go +++ b/pkg/types/authtypes/selector.go @@ -24,7 +24,7 @@ var ( typeRoleSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`) 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}$`) + 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 typeMetaResourcesSelectorRegex = regexp.MustCompile(`^\*$`) ) diff --git a/pkg/types/roletypes/role.go b/pkg/types/roletypes/role.go index 2f4e170985..1dbada2426 100644 --- a/pkg/types/roletypes/role.go +++ b/pkg/types/roletypes/role.go @@ -20,6 +20,7 @@ var ( ErrCodeInvalidTypeRelation = errors.MustNewCode("role_invalid_type_relation") ErrCodeRoleNotFound = errors.MustNewCode("role_not_found") ErrCodeRoleFailedTransactionsFromString = errors.MustNewCode("role_failed_transactions_from_string") + ErrCodeRoleUnsupported = errors.MustNewCode("role_unsupported") ) var ( @@ -32,8 +33,22 @@ var ( ) var ( - AnonymousUserRoleName = "signoz-anonymous" - AnonymousUserRoleDescription = "Role assigned to anonymous users for access to public resources." + SigNozAnonymousRoleName = "signoz-anonymous" + SigNozAnonymousRoleDescription = "Role assigned to anonymous users for access to public resources." + SigNozAdminRoleName = "signoz-admin" + SigNozAdminRoleDescription = "Role assigned to users who have full administrative access to SigNoz resources." + SigNozEditorRoleName = "signoz-editor" + SigNozEditorRoleDescription = "Role assigned to users who can create, edit, and manage SigNoz resources but do not have full administrative privileges." + SigNozViewerRoleName = "signoz-viewer" + SigNozViewerRoleDescription = "Role assigned to users who have read-only access to SigNoz resources." +) + +var ( + ExistingRoleToSigNozManagedRoleMap = map[types.Role]string{ + types.RoleAdmin: SigNozAdminRoleName, + types.RoleEditor: SigNozEditorRoleName, + types.RoleViewer: SigNozViewerRoleName, + } ) var ( @@ -54,10 +69,10 @@ type StorableRole struct { type Role struct { types.Identifiable types.TimeAuditable - Name string `json:"name"` - Description string `json:"description"` - Type string `json:"type"` - OrgID valuer.UUID `json:"org_id"` + Name string `json:"name"` + Description string `json:"description"` + Type valuer.String `json:"type"` + OrgID valuer.UUID `json:"orgId"` } type PostableRole struct { @@ -81,7 +96,7 @@ func NewStorableRoleFromRole(role *Role) *StorableRole { TimeAuditable: role.TimeAuditable, Name: role.Name, Description: role.Description, - Type: role.Type, + Type: role.Type.String(), OrgID: role.OrgID.StringValue(), } } @@ -92,12 +107,12 @@ func NewRoleFromStorableRole(storableRole *StorableRole) *Role { TimeAuditable: storableRole.TimeAuditable, Name: storableRole.Name, Description: storableRole.Description, - Type: storableRole.Type, + Type: valuer.NewString(storableRole.Type), OrgID: valuer.MustNewUUID(storableRole.OrgID), } } -func NewRole(name, description string, roleType string, orgID valuer.UUID) *Role { +func NewRole(name, description string, roleType valuer.String, orgID valuer.UUID) *Role { return &Role{ Identifiable: types.Identifiable{ ID: valuer.GenerateUUID(), @@ -113,7 +128,38 @@ func NewRole(name, description string, roleType string, orgID valuer.UUID) *Role } } -func NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.Object, relation authtypes.Relation) (*PatchableObjects, error) { +func NewManagedRoles(orgID valuer.UUID) []*Role { + return []*Role{ + NewRole(SigNozAdminRoleName, SigNozAdminRoleDescription, RoleTypeManaged, orgID), + NewRole(SigNozEditorRoleName, SigNozEditorRoleDescription, RoleTypeManaged, orgID), + NewRole(SigNozViewerRoleName, SigNozViewerRoleDescription, RoleTypeManaged, orgID), + NewRole(SigNozAnonymousRoleName, SigNozAnonymousRoleDescription, RoleTypeManaged, orgID), + } + +} + +func (role *Role) PatchMetadata(name, description *string) error { + err := role.CanEditDelete() + if err != nil { + return err + } + + if name != nil { + role.Name = *name + } + if description != nil { + role.Description = *description + } + role.UpdatedAt = time.Now() + return nil +} + +func (role *Role) NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.Object, relation authtypes.Relation) (*PatchableObjects, error) { + err := role.CanEditDelete() + if err != nil { + return nil, err + } + if len(additions) == 0 && len(deletions) == 0 { return nil, errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty object patch request received, at least one of additions or deletions must be present") } @@ -133,14 +179,12 @@ func NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.O return &PatchableObjects{Additions: additions, Deletions: deletions}, nil } -func (role *Role) PatchMetadata(name, description *string) { - if name != nil { - role.Name = *name +func (role *Role) CanEditDelete() error { + if role.Type == RoleTypeManaged { + return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "cannot edit/delete managed role: %s", role.Name) } - if description != nil { - role.Description = *description - } - role.UpdatedAt = time.Now() + + return nil } func (role *PostableRole) UnmarshalJSON(data []byte) error { @@ -246,3 +290,12 @@ func GetDeletionTuples(id valuer.UUID, orgID valuer.UUID, relation authtypes.Rel return tuples, nil } + +func MustGetSigNozManagedRoleFromExistingRole(role types.Role) string { + managedRole, ok := ExistingRoleToSigNozManagedRoleMap[role] + if !ok { + panic(errors.Newf(errors.TypeInternal, errors.CodeInternal, "invalid role: %s", role.String())) + } + + return managedRole +} diff --git a/pkg/types/roletypes/store.go b/pkg/types/roletypes/store.go index 5fbbff3032..ba2c5256ef 100644 --- a/pkg/types/roletypes/store.go +++ b/pkg/types/roletypes/store.go @@ -9,8 +9,9 @@ import ( type Store interface { Create(context.Context, *StorableRole) error Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error) - GetByNameAndOrgID(context.Context, string, valuer.UUID) (*StorableRole, error) + GetByOrgIDAndName(context.Context, valuer.UUID, string) (*StorableRole, error) List(context.Context, valuer.UUID) ([]*StorableRole, error) + ListByOrgIDAndNames(context.Context, valuer.UUID, []string) ([]*StorableRole, error) Update(context.Context, valuer.UUID, *StorableRole) error Delete(context.Context, valuer.UUID, valuer.UUID) error RunInTx(context.Context, func(ctx context.Context) error) error diff --git a/tests/integration/fixtures/auth.py b/tests/integration/fixtures/auth.py index 762f4c6ee9..952a17322d 100644 --- a/tests/integration/fixtures/auth.py +++ b/tests/integration/fixtures/auth.py @@ -20,6 +20,10 @@ USER_ADMIN_NAME = "admin" USER_ADMIN_EMAIL = "admin@integration.test" USER_ADMIN_PASSWORD = "password123Z$" +USER_EDITOR_NAME = 'editor' +USER_EDITOR_EMAIL = 'editor@integration.test' +USER_EDITOR_PASSWORD = 'password123Z$' + @pytest.fixture(name="create_user_admin", scope="package") def create_user_admin(