feat(authz): build role module (#9136)

* feat(authz): build role module

* feat(authz): build role module

* feat(authz): refactor the role module to move transactions out

* feat(authz): add handler implementation except patch objects

* feat(authz): added the missing handler

* feat(authz): added changes for selectors

* feat(authz): added changes for selectors

* feat(authz): added changes for selectors

* feat(authz): make the role create handler just to create metadata

* feat(authz): address review comments

* feat(authz): address review comments

* feat(authz): address review comments

* feat(authz): address review comments
This commit is contained in:
Vikrant Gupta 2025-09-29 17:45:52 +05:30 committed by GitHub
parent 3c3641493e
commit 1b818dd05d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1463 additions and 221 deletions

View File

@ -26,10 +26,6 @@ type resources
define create: [user, role#assignee]
define list: [user, role#assignee]
define read: [user, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
type resource
relations
define read: [user, anonymous, role#assignee]

View File

@ -107,7 +107,7 @@ func (middleware *AuthZ) OpenAccess(next http.HandlerFunc) http.HandlerFunc {
}
// Check middleware accepts the relation, typeable, parentTypeable (for direct access + group relations) and a callback function to derive selector and parentSelectors on per request basis.
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, parentTypeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc {
func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relation, translation authtypes.Relation, typeable authtypes.Typeable, cb authtypes.SelectorCallbackFn) http.HandlerFunc {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
claims, err := authtypes.ClaimsFromContext(req.Context())
if err != nil {
@ -115,13 +115,13 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, relation authtypes.Relatio
return
}
selector, parentSelectors, err := cb(req)
selector, err := cb(req.Context(), claims)
if err != nil {
render.Error(rw, err)
return
}
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, relation, typeable, selector, parentTypeable, parentSelectors...)
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, relation, typeable, selector)
if err != nil {
render.Error(rw, err)
return

View File

@ -12,8 +12,14 @@ type AuthZ interface {
factory.Service
// Check returns error when the upstream authorization server is unavailable or the subject (s) doesn't have relation (r) on object (o).
Check(context.Context, *openfgav1.CheckRequestTupleKey) error
Check(context.Context, *openfgav1.TupleKey) error
// CheckWithTupleCreation takes upon the responsibility for generating the tuples alongside everything Check does.
CheckWithTupleCreation(context.Context, authtypes.Claims, authtypes.Relation, authtypes.Typeable, authtypes.Selector, authtypes.Typeable, ...authtypes.Selector) error
CheckWithTupleCreation(context.Context, authtypes.Claims, authtypes.Relation, authtypes.Typeable, []authtypes.Selector) error
// writes the tuples to upstream server
Write(context.Context, *openfgav1.WriteRequest) error
// lists the selectors for objects assigned to subject (s) with relation (r) on resource (s)
ListObjects(context.Context, string, authtypes.Relation, authtypes.Typeable) ([]*authtypes.Object, error)
}

View File

@ -176,13 +176,17 @@ func (provider *provider) isModelEqual(expected *openfgav1.AuthorizationModel, a
}
func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.CheckRequestTupleKey) error {
func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKey) error {
checkResponse, err := provider.openfgaServer.Check(
ctx,
&openfgav1.CheckRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
TupleKey: tupleReq,
TupleKey: &openfgav1.CheckRequestTupleKey{
User: tupleReq.User,
Relation: tupleReq.Relation,
Object: tupleReq.Object,
},
})
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
@ -195,39 +199,79 @@ func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.CheckRe
return nil
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, relation authtypes.Relation, typeable authtypes.Typeable, selector authtypes.Selector, parentTypeable authtypes.Typeable, parentSelectors ...authtypes.Selector) error {
func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.TupleKey) error {
batchCheckItems := make([]*openfgav1.BatchCheckItem, 0)
for _, tuple := range tupleReq {
batchCheckItems = append(batchCheckItems, &openfgav1.BatchCheckItem{
TupleKey: &openfgav1.CheckRequestTupleKey{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
},
})
}
checkResponse, err := provider.openfgaServer.BatchCheck(
ctx,
&openfgav1.BatchCheckRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
Checks: batchCheckItems,
})
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
}
for _, checkResponse := range checkResponse.Result {
if checkResponse.GetAllowed() {
return nil
}
}
return errors.New(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "")
}
func (provider *provider) CheckWithTupleCreation(ctx context.Context, claims authtypes.Claims, relation authtypes.Relation, typeable authtypes.Typeable, selectors []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeUser, claims.UserID, authtypes.Relation{})
if err != nil {
return err
}
tuples, err := typeable.Tuples(subject, relation, selector, parentTypeable, parentSelectors...)
tuples, err := typeable.Tuples(subject, relation, selectors)
if err != nil {
return err
}
check, err := provider.sequentialCheck(ctx, tuples)
err = provider.BatchCheck(ctx, tuples)
if err != nil {
return err
}
if !check {
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subject %s cannot %s object %s", subject, relation.StringValue(), typeable.Type().StringValue())
}
return nil
}
func (provider *provider) sequentialCheck(ctx context.Context, tuplesReq []*openfgav1.CheckRequestTupleKey) (bool, error) {
for _, tupleReq := range tuplesReq {
err := provider.Check(ctx, tupleReq)
if err == nil {
return true, nil
}
if errors.Ast(err, errors.TypeInternal) {
// return at the first internal error as the evaluation will be incorrect
return false, err
}
func (provider *provider) Write(ctx context.Context, req *openfgav1.WriteRequest) error {
_, err := provider.openfgaServer.Write(ctx, &openfgav1.WriteRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
Writes: req.Writes,
})
return err
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
response, err := provider.openfgaServer.ListObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
User: subject,
Relation: relation.StringValue(),
Type: typeable.Type().StringValue(),
})
if err != nil {
return nil, errors.Wrapf(err, errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "cannot list objects for subject %s with relation %s for type %s", subject, relation.StringValue(), typeable.Type().StringValue())
}
return false, nil
return authtypes.MustNewObjectsFromStringSlice(response.Objects), nil
}

View File

@ -114,7 +114,7 @@ func (middleware *AuthZ) Check(next http.HandlerFunc, _ authtypes.Relation, tran
return
}
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, translation, authtypes.TypeableOrganization, authtypes.MustNewSelector(authtypes.TypeOrganization, claims.OrgID), nil)
err = middleware.authzService.CheckWithTupleCreation(req.Context(), claims, translation, authtypes.TypeableOrganization, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, claims.OrgID)})
if err != nil {
render.Error(rw, err)
return

View File

@ -4,6 +4,7 @@ import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/statsreporter"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
@ -26,6 +27,7 @@ type Module interface {
GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]map[string]string, error)
statsreporter.StatsCollector
role.RegisterTypeable
}
type Handler interface {

View File

@ -10,6 +10,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@ -222,3 +223,7 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return dashboardtypes.NewStatsFromStorableDashboards(dashboards), nil
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{dashboardtypes.ResourceDashboard, dashboardtypes.ResourcesDashboards}
}

View File

@ -0,0 +1,291 @@
package implrole
import (
"net/http"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/http/binding"
"github.com/SigNoz/signoz/pkg/http/render"
"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"
"github.com/gorilla/mux"
)
type handler struct {
module role.Module
}
func NewHandler(module role.Module) (role.Handler, error) {
return &handler{module: module}, nil
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
req := new(roletypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
role, err := handler.module.Create(ctx, orgID, req.DisplayName, req.Description)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, role.ID.StringValue())
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
role, err := handler.module.Get(ctx, orgID, roleID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, role)
}
func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
relationStr, ok := mux.Vars(r)["relation"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := authtypes.NewRelation(relationStr)
if err != nil {
render.Error(rw, err)
return
}
objects, err := handler.module.GetObjects(ctx, orgID, roleID, relation)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, objects)
}
func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resources := handler.module.GetResources(ctx)
var resourceRelations = struct {
Resources []*authtypes.Resource `json:"resources"`
Relations map[authtypes.Type][]authtypes.Relation `json:"relations"`
}{
Resources: resources,
Relations: authtypes.TypeableRelations,
}
render.Success(rw, http.StatusOK, resourceRelations)
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
roles, err := handler.module.List(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, roles)
}
func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
req := new(roletypes.PatchableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
err = handler.module.Patch(ctx, orgID, roleID, req.DisplayName, req.Description)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
relationStr, ok := mux.Vars(r)["relation"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "relation is missing from the request"))
return
}
relation, err := authtypes.NewRelation(relationStr)
if err != nil {
render.Error(rw, err)
return
}
req := new(roletypes.PatchableObjects)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
patchableObjects, err := roletypes.NewPatchableObjects(req.Additions, req.Deletions, relation)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.PatchObjects(ctx, orgID, roleID, relation, patchableObjects.Additions, patchableObjects.Deletions)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusAccepted, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
if err != nil {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, ok := mux.Vars(r)["id"]
if !ok {
render.Error(rw, errors.New(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "id is missing from the request"))
return
}
roleID, err := valuer.NewUUID(id)
if err != nil {
render.Error(rw, err)
return
}
err = handler.module.Delete(ctx, orgID, roleID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}

View File

@ -0,0 +1,172 @@
package implrole
import (
"context"
"slices"
"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"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
type module struct {
store roletypes.Store
registry []role.RegisterTypeable
authz authz.AuthZ
}
func NewModule(ctx context.Context, store roletypes.Store, authz authz.AuthZ, registry []role.RegisterTypeable) (role.Module, error) {
return &module{
store: store,
authz: authz,
registry: registry,
}, nil
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, displayName, description string) (*roletypes.Role, error) {
role := roletypes.NewRole(displayName, description, orgID)
storableRole, err := roletypes.NewStorableRoleFromRole(role)
if err != nil {
return nil, err
}
err = module.store.Create(ctx, storableRole)
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()...)
}
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
}
role, err := roletypes.NewRoleFromStorableRole(storableRole)
if err != nil {
return nil, err
}
return role, 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.TypeRole, storableRole.ID.String(), 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 {
role, err := roletypes.NewRoleFromStorableRole(storableRole)
if err != nil {
return nil, err
}
roles[idx] = role
}
return roles, nil
}
func (module *module) Patch(ctx context.Context, orgID valuer.UUID, id valuer.UUID, displayName, description *string) error {
storableRole, err := module.store.Get(ctx, orgID, id)
if err != nil {
return err
}
role, err := roletypes.NewRoleFromStorableRole(storableRole)
if err != nil {
return err
}
role.PatchMetadata(displayName, description)
updatedRole, err := roletypes.NewStorableRoleFromRole(role)
if err != nil {
return err
}
err = module.store.Update(ctx, orgID, updatedRole)
if err != nil {
return err
}
return nil
}
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, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(id, relation, deletions)
if err != nil {
return err
}
err = module.authz.Write(ctx, &openfgav1.WriteRequest{
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: additionTuples,
},
Deletes: &openfgav1.WriteRequestDeletes{
TupleKeys: deletionTuples,
},
})
if err != nil {
return err
}
return nil
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
return module.store.Delete(ctx, orgID, id)
}

View File

@ -0,0 +1,103 @@
package implrole
import (
"context"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) (roletypes.Store, error) {
return &store{sqlstore: sqlstore}, nil
}
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
NewInsert().
Model(role).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapAlreadyExistsErrf(err, errors.CodeAlreadyExists, "role with id: %s already exists", role.ID)
}
return nil
}
func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.StorableRole, error) {
role := new(roletypes.StorableRole)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(role).
Where("orgID = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id: %s doesn't exist", id)
}
return role, nil
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDB().
NewSelect().
Model(&roles).
Where("orgID = ?", orgID).
Scan(ctx)
if err != nil {
return nil, store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "no roles found in org_id: %s", orgID)
}
return roles, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
NewUpdate().
Model(role).
WherePK().
Where("org_id = ?", orgID).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, errors.CodeAlreadyExists, "role with id %s doesn't exist", role.ID)
}
return nil
}
func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDB().
NewDelete().
Model(new(roletypes.StorableRole)).
Where("org_id = ?", orgID).
Where("id = ?", id).
Exec(ctx)
if err != nil {
return store.sqlstore.WrapNotFoundErrf(err, roletypes.ErrCodeRoleNotFound, "role with id %s doesn't exist", id)
}
return nil
}
func (store *store) RunInTx(ctx context.Context, cb func(ctx context.Context) error) error {
return store.sqlstore.RunInTxCtx(ctx, nil, func(ctx context.Context) error {
return cb(ctx)
})
}

66
pkg/modules/role/role.go Normal file
View File

@ -0,0 +1,66 @@
package role
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
// Creates the role metadata
Create(context.Context, valuer.UUID, string, string) (*roletypes.Role, error)
// Gets the role metadata
Get(context.Context, valuer.UUID, valuer.UUID) (*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 metadata 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
// Patches the roles metadata
Patch(context.Context, valuer.UUID, valuer.UUID, *string, *string) error
// Patches the objects in authorization server associated with the given role and relation
PatchObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation, []*authtypes.Object, []*authtypes.Object) error
// Deletes the role metadata and tuples in authorization server
Delete(context.Context, valuer.UUID, valuer.UUID) error
}
type RegisterTypeable interface {
MustGetTypeables() []authtypes.Typeable
}
type Handler interface {
// Creates the role metadata and tuples in authorization server
Create(http.ResponseWriter, *http.Request)
// Gets the role metadata
Get(http.ResponseWriter, *http.Request)
// Gets the objects for the given relation and role
GetObjects(http.ResponseWriter, *http.Request)
// Gets all the resources and the relations
GetResources(http.ResponseWriter, *http.Request)
// Lists all the roles metadata for the organization
List(http.ResponseWriter, *http.Request)
// Patches the role metdata
Patch(http.ResponseWriter, *http.Request)
// Patches the objects for the given relation and role
PatchObjects(http.ResponseWriter, *http.Request)
// Deletes the role metadata and tuples in authorization server
Delete(http.ResponseWriter, *http.Request)
}

View File

@ -1,6 +1,7 @@
package authtypes
import (
"encoding/json"
"regexp"
"github.com/SigNoz/signoz/pkg/errors"
@ -14,14 +15,39 @@ type Name struct {
val string
}
func MustNewName(name string) Name {
func NewName(name string) (Name, error) {
if !nameRegex.MatchString(name) {
panic(errors.NewInternalf(errors.CodeInternal, "name must conform to regex %s", nameRegex.String()))
return Name{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "name must conform to regex %s", nameRegex.String())
}
return Name{val: name}
return Name{val: name}, nil
}
func MustNewName(name string) Name {
named, err := NewName(name)
if err != nil {
panic(err)
}
return named
}
func (name Name) String() string {
return name.val
}
func (name *Name) UnmarshalJSON(data []byte) error {
nameStr := ""
err := json.Unmarshal(data, &nameStr)
if err != nil {
return err
}
shadow, err := NewName(nameStr)
if err != nil {
return err
}
*name = shadow
return nil
}

View File

@ -1,23 +0,0 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(organization)
type organization struct{}
func (organization *organization) Tuples(subject string, relation Relation, selector Selector, parentTypeable Typeable, parentSelectors ...Selector) ([]*openfgav1.CheckRequestTupleKey, error) {
tuples := make([]*openfgav1.CheckRequestTupleKey, 0)
object := strings.Join([]string{TypeRole.StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.CheckRequestTupleKey{User: subject, Relation: relation.StringValue(), Object: object})
return tuples, nil
}
func (organization *organization) Type() Type {
return TypeOrganization
}

View File

@ -1,6 +1,13 @@
package authtypes
import "github.com/SigNoz/signoz/pkg/valuer"
import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
)
var (
ErrCodeAuthZInvalidRelation = errors.MustNewCode("authz_invalid_relation")
)
var (
RelationCreate = Relation{valuer.NewString("create")}
@ -12,12 +19,33 @@ var (
RelationAssignee = Relation{valuer.NewString("assignee")}
)
var (
TypeUserSupportedRelations = []Relation{RelationRead, RelationUpdate, RelationDelete}
TypeRoleSupportedRelations = []Relation{RelationAssignee, RelationRead, RelationUpdate, RelationDelete}
TypeOrganizationSupportedRelations = []Relation{RelationCreate, RelationRead, RelationUpdate, RelationDelete, RelationList}
TypeResourceSupportedRelations = []Relation{RelationRead, RelationUpdate, RelationDelete, RelationBlock}
TypeResourcesSupportedRelations = []Relation{RelationCreate, RelationRead, RelationUpdate, RelationDelete, RelationList}
)
var TypeableRelations = map[Type][]Relation{
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
TypeOrganization: {RelationCreate, RelationRead, RelationUpdate, RelationDelete, RelationList},
TypeResource: {RelationRead, RelationUpdate, RelationDelete, RelationBlock},
TypeResources: {RelationCreate, RelationList},
}
type Relation struct{ valuer.String }
func NewRelation(relation string) (Relation, error) {
switch relation {
case "create":
return RelationCreate, nil
case "read":
return RelationRead, nil
case "update":
return RelationUpdate, nil
case "delete":
return RelationDelete, nil
case "list":
return RelationList, nil
case "block":
return RelationBlock, nil
case "assignee":
return RelationAssignee, nil
default:
return Relation{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidRelation, "invalid relation %s", relation)
}
}

View File

@ -1,37 +0,0 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(resource)
type resource struct {
name Name
}
func MustNewResource(name string) Typeable {
return &resource{name: MustNewName(name)}
}
func (resource *resource) Tuples(subject string, relation Relation, selector Selector, parentTypeable Typeable, parentSelectors ...Selector) ([]*openfgav1.CheckRequestTupleKey, error) {
tuples := make([]*openfgav1.CheckRequestTupleKey, 0)
for _, selector := range parentSelectors {
resourcesTuples, err := parentTypeable.Tuples(subject, relation, selector, nil)
if err != nil {
return nil, err
}
tuples = append(tuples, resourcesTuples...)
}
object := strings.Join([]string{TypeResource.StringValue(), resource.name.String(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.CheckRequestTupleKey{User: subject, Relation: relation.StringValue(), Object: object})
return tuples, nil
}
func (resource *resource) Type() Type {
return TypeResource
}

View File

@ -1,26 +0,0 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(resources)
type resources struct {
name Name
}
func MustNewResources(name string) Typeable {
return &resources{name: MustNewName(name)}
}
func (resources *resources) Tuples(subject string, relation Relation, selector Selector, _ Typeable, _ ...Selector) ([]*openfgav1.CheckRequestTupleKey, error) {
object := strings.Join([]string{TypeResources.StringValue(), resources.name.String(), selector.String()}, ":")
return []*openfgav1.CheckRequestTupleKey{{User: subject, Relation: relation.StringValue(), Object: object}}, nil
}
func (resources *resources) Type() Type {
return TypeResources
}

View File

@ -1,31 +0,0 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(role)
type role struct{}
func (role *role) Tuples(subject string, relation Relation, selector Selector, parentTypeable Typeable, parentSelectors ...Selector) ([]*openfgav1.CheckRequestTupleKey, error) {
tuples := make([]*openfgav1.CheckRequestTupleKey, 0)
for _, selector := range parentSelectors {
resourcesTuples, err := parentTypeable.Tuples(subject, relation, selector, nil)
if err != nil {
return nil, err
}
tuples = append(tuples, resourcesTuples...)
}
object := strings.Join([]string{TypeRole.StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.CheckRequestTupleKey{User: subject, Relation: relation.StringValue(), Object: object})
return tuples, nil
}
func (role *role) Type() Type {
return TypeRole
}

View File

@ -1,53 +1,67 @@
package authtypes
import (
"net/http"
"context"
"encoding/json"
"regexp"
"github.com/SigNoz/signoz/pkg/errors"
)
var (
ErrCodeAuthZInvalidSelectorRegex = errors.MustNewCode("authz_invalid_selector_regex")
)
var (
typeUserSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeRoleSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeOrganizationSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeResourceSelectorRegex = regexp.MustCompile(`^[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeResourcesSelectorRegex = regexp.MustCompile(`^org:[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
typeResourcesSelectorRegex = regexp.MustCompile(`^org/[0-9a-f]{8}(?:\-[0-9a-f]{4}){3}-[0-9a-f]{12}$`)
)
type SelectorCallbackFn func(*http.Request) (Selector, []Selector, error)
type SelectorCallbackFn func(context.Context, Claims) ([]Selector, error)
type Selector struct {
val string
}
func NewSelector(typed Type, selector string) (Selector, error) {
switch typed {
case TypeUser:
if !typeUserSelectorRegex.MatchString(selector) {
return Selector{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeUserSelectorRegex.String())
}
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector) {
return Selector{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeRoleSelectorRegex.String())
}
case TypeOrganization:
if !typeOrganizationSelectorRegex.MatchString(selector) {
return Selector{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeOrganizationSelectorRegex.String())
}
case TypeResource:
if !typeResourceSelectorRegex.MatchString(selector) {
return Selector{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeResourceSelectorRegex.String())
}
case TypeResources:
if !typeResourcesSelectorRegex.MatchString(selector) {
return Selector{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeResourcesSelectorRegex.String())
}
err := IsValidSelector(typed, Selector{val: selector})
if err != nil {
return Selector{}, err
}
return Selector{val: selector}, nil
}
func IsValidSelector(typed Type, selector Selector) error {
switch typed {
case TypeUser:
if !typeUserSelectorRegex.MatchString(selector.String()) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeUserSelectorRegex.String())
}
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector.String()) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeRoleSelectorRegex.String())
}
case TypeOrganization:
if !typeOrganizationSelectorRegex.MatchString(selector.String()) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeOrganizationSelectorRegex.String())
}
case TypeResource:
if !typeResourceSelectorRegex.MatchString(selector.String()) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeResourceSelectorRegex.String())
}
case TypeResources:
if !typeResourcesSelectorRegex.MatchString(selector.String()) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeResourcesSelectorRegex.String())
}
}
return nil
}
func MustNewSelector(typed Type, input string) Selector {
selector, err := NewSelector(typed, input)
if err != nil {
@ -60,3 +74,16 @@ func MustNewSelector(typed Type, input string) Selector {
func (selector Selector) String() string {
return selector.val
}
func (typed *Selector) UnmarshalJSON(data []byte) error {
str := ""
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
shadow := Selector{val: str}
*typed = shadow
return nil
}

View File

@ -0,0 +1,108 @@
package authtypes
import (
"encoding/json"
"slices"
"strings"
"github.com/SigNoz/signoz/pkg/errors"
)
type Resource struct {
Name Name `json:"name"`
Type Type `json:"type"`
}
type Object struct {
Resource Resource `json:"resource"`
Selector Selector `json:"selector"`
}
type Transaction struct {
Relation Relation `json:"relation"`
Object Object `json:"object"`
}
func NewObject(resource Resource, selector Selector) (*Object, error) {
err := IsValidSelector(resource.Type, selector)
if err != nil {
return nil, err
}
return &Object{Resource: resource, Selector: selector}, nil
}
func MustNewObjectFromString(input string) *Object {
parts := strings.Split(input, ":")
if len(parts) != 3 {
panic(errors.Newf(errors.TypeInternal, errors.CodeInternal, "invalid list objects output: %s", input))
}
resource := Resource{
Type: MustNewType(parts[0]),
Name: MustNewName(parts[1]),
}
object := &Object{
Resource: resource,
Selector: MustNewSelector(resource.Type, parts[2]),
}
return object
}
func MustNewObjectsFromStringSlice(input []string) []*Object {
objects := make([]*Object, 0, len(input))
for _, str := range input {
objects = append(objects, MustNewObjectFromString(str))
}
return objects
}
func (object *Object) UnmarshalJSON(data []byte) error {
var shadow = struct {
Resource Resource
Selector Selector
}{}
err := json.Unmarshal(data, &shadow)
if err != nil {
return err
}
obj, err := NewObject(shadow.Resource, shadow.Selector)
if err != nil {
return err
}
*object = *obj
return nil
}
func NewTransaction(relation Relation, object Object) (*Transaction, error) {
if !slices.Contains(TypeableRelations[object.Resource.Type], relation) {
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidRelation, "invalid relation %s for type %s", relation.StringValue(), object.Resource.Type.StringValue())
}
return &Transaction{Relation: relation, Object: object}, nil
}
func (transaction *Transaction) UnmarshalJSON(data []byte) error {
var shadow = struct {
Relation Relation
Object Object
}{}
err := json.Unmarshal(data, &shadow)
if err != nil {
return err
}
txn, err := NewTransaction(shadow.Relation, shadow.Object)
if err != nil {
return err
}
*transaction = *txn
return nil
}

View File

@ -1,17 +1,16 @@
package authtypes
import (
"encoding/json"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var (
ErrCodeAuthZUnavailable = errors.MustNewCode("authz_unavailable")
ErrCodeAuthZForbidden = errors.MustNewCode("authz_forbidden")
ErrCodeAuthZInvalidSelectorRegex = errors.MustNewCode("authz_invalid_selector_regex")
ErrCodeAuthZUnsupportedRelation = errors.MustNewCode("authz_unsupported_relation")
ErrCodeAuthZInvalidSubject = errors.MustNewCode("authz_invalid_subject")
ErrCodeAuthZUnavailable = errors.MustNewCode("authz_unavailable")
ErrCodeAuthZForbidden = errors.MustNewCode("authz_forbidden")
)
var (
@ -23,14 +22,92 @@ var (
)
var (
TypeableUser = &user{}
TypeableRole = &role{}
TypeableOrganization = &organization{}
TypeableUser = &typeableUser{}
TypeableRole = &typeableRole{}
TypeableOrganization = &typeableOrganization{}
)
type Typeable interface {
Type() Type
Tuples(subject string, relation Relation, selector Selector, parentType Typeable, parentSelectors ...Selector) ([]*openfgav1.CheckRequestTupleKey, error)
Name() Name
Prefix() string
Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error)
}
type Type struct{ valuer.String }
func MustNewType(input string) Type {
typed, err := NewType(input)
if err != nil {
panic(err)
}
return typed
}
func NewType(input string) (Type, error) {
switch input {
case "user":
return TypeUser, nil
case "role":
return TypeRole, nil
case "organization":
return TypeOrganization, nil
case "resource":
return TypeResource, nil
case "resources":
return TypeResources, nil
default:
return Type{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid type: %s", input)
}
}
func (typed *Type) UnmarshalJSON(data []byte) error {
str := ""
err := json.Unmarshal(data, &str)
if err != nil {
return err
}
shadow, err := NewType(str)
if err != nil {
return err
}
*typed = shadow
return nil
}
func NewTypeableFromType(typed Type, name Name) (Typeable, error) {
switch typed {
case TypeRole:
return TypeableRole, nil
case TypeUser:
return TypeableUser, nil
case TypeOrganization:
return TypeableOrganization, nil
case TypeResource:
resource, err := NewTypeableResource(name)
if err != nil {
return nil, err
}
return resource, nil
case TypeResources:
resources, err := NewTypeableResources(name)
if err != nil {
return nil, err
}
return resources, nil
}
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid type")
}
func MustNewTypeableFromType(typed Type, name Name) Typeable {
typeable, err := NewTypeableFromType(typed, name)
if err != nil {
panic(err)
}
return typeable
}

View File

@ -0,0 +1,33 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableOrganization)
type typeableOrganization struct{}
func (typeableOrganization *typeableOrganization) Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := strings.Join([]string{typeableOrganization.Type().StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableOrganization *typeableOrganization) Type() Type {
return TypeOrganization
}
func (typeableOrganization *typeableOrganization) Name() Name {
return MustNewName("organization")
}
func (typeableOrganization *typeableOrganization) Prefix() string {
return typeableOrganization.Type().StringValue()
}

View File

@ -0,0 +1,47 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableResource)
type typeableResource struct {
name Name
}
func NewTypeableResource(name Name) (Typeable, error) {
return &typeableResource{name: name}, nil
}
func MustNewTypeableResource(name Name) Typeable {
typeableesource, err := NewTypeableResource(name)
if err != nil {
panic(err)
}
return typeableesource
}
func (typeableResource *typeableResource) Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableResource.Prefix() + "/" + selector.String()
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableResource *typeableResource) Type() Type {
return TypeResource
}
func (typeableResource *typeableResource) Name() Name {
return typeableResource.name
}
func (typeableResource *typeableResource) Prefix() string {
return strings.Join([]string{typeableResource.Type().StringValue(), typeableResource.Name().String()}, ":")
}

View File

@ -0,0 +1,47 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableResources)
type typeableResources struct {
name Name
}
func NewTypeableResources(name Name) (Typeable, error) {
return &typeableResources{name: name}, nil
}
func MustNewTypeableResources(name Name) Typeable {
resources, err := NewTypeableResources(name)
if err != nil {
panic(err)
}
return resources
}
func (typeableResources *typeableResources) Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableResources.Prefix() + "/" + selector.String()
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableResources *typeableResources) Type() Type {
return TypeResources
}
func (typeableResources *typeableResources) Name() Name {
return typeableResources.name
}
func (typeableResources *typeableResources) Prefix() string {
return strings.Join([]string{typeableResources.Type().StringValue(), typeableResources.Name().String()}, ":")
}

View File

@ -0,0 +1,33 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableRole)
type typeableRole struct{}
func (typeableRole *typeableRole) Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := strings.Join([]string{typeableRole.Type().StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableRole *typeableRole) Type() Type {
return TypeRole
}
func (typeableRole *typeableRole) Name() Name {
return MustNewName("role")
}
func (typeableRole *typeableRole) Prefix() string {
return typeableRole.Type().StringValue()
}

View File

@ -0,0 +1,33 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableUser)
type typeableUser struct{}
func (typeableUser *typeableUser) Tuples(subject string, relation Relation, selector []Selector) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := strings.Join([]string{typeableUser.Type().StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableUser *typeableUser) Type() Type {
return TypeUser
}
func (typeableUser *typeableUser) Name() Name {
return MustNewName("user")
}
func (typeableUser *typeableUser) Prefix() string {
return typeableUser.Type().StringValue()
}

View File

@ -1,31 +0,0 @@
package authtypes
import (
"strings"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(user)
type user struct{}
func (user *user) Tuples(subject string, relation Relation, selector Selector, parentTypeable Typeable, parentSelectors ...Selector) ([]*openfgav1.CheckRequestTupleKey, error) {
tuples := make([]*openfgav1.CheckRequestTupleKey, 0)
for _, selector := range parentSelectors {
resourcesTuples, err := parentTypeable.Tuples(subject, relation, selector, nil)
if err != nil {
return nil, err
}
tuples = append(tuples, resourcesTuples...)
}
object := strings.Join([]string{TypeUser.StringValue(), selector.String()}, ":")
tuples = append(tuples, &openfgav1.CheckRequestTupleKey{User: subject, Relation: relation.StringValue(), Object: object})
return tuples, nil
}
func (user *user) Type() Type {
return TypeUser
}

View File

@ -7,10 +7,16 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
var (
ResourceDashboard = authtypes.MustNewTypeableResource(authtypes.MustNewName("dashboard"))
ResourcesDashboards = authtypes.MustNewTypeableResources(authtypes.MustNewName("dashboards"))
)
type StorableDashboard struct {
bun.BaseModel `bun:"table:dashboard"`

224
pkg/types/roletypes/role.go Normal file
View File

@ -0,0 +1,224 @@
package roletypes
import (
"encoding/json"
"slices"
"time"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
"github.com/uptrace/bun"
)
var (
ErrCodeRoleInvalidInput = errors.MustNewCode("role_invalid_input")
ErrCodeRoleEmptyPatch = errors.MustNewCode("role_empty_patch")
ErrCodeInvalidTypeRelation = errors.MustNewCode("role_invalid_type_relation")
ErrCodeRoleNotFound = errors.MustNewCode("role_not_found")
ErrCodeRoleFailedTransactionsFromString = errors.MustNewCode("role_failed_transactions_from_string")
)
type StorableRole struct {
bun.BaseModel `bun:"table:role"`
types.Identifiable
types.TimeAuditable
DisplayName string `bun:"display_name,type:string"`
Description string `bun:"description,type:string"`
OrgID string `bun:"org_id,type:string"`
}
type Role struct {
types.Identifiable
types.TimeAuditable
DisplayName string `json:"displayName"`
Description string `json:"description"`
OrgID valuer.UUID `json:"org_id"`
}
type PostableRole struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
type PatchableRole struct {
DisplayName *string `json:"displayName"`
Description *string `json:"description"`
}
type PatchableObjects struct {
Additions []*authtypes.Object `json:"additions"`
Deletions []*authtypes.Object `json:"deletions"`
}
func NewStorableRoleFromRole(role *Role) (*StorableRole, error) {
return &StorableRole{
Identifiable: role.Identifiable,
TimeAuditable: role.TimeAuditable,
DisplayName: role.DisplayName,
Description: role.Description,
OrgID: role.OrgID.StringValue(),
}, nil
}
func NewRoleFromStorableRole(storableRole *StorableRole) (*Role, error) {
orgID, err := valuer.NewUUID(storableRole.OrgID)
if err != nil {
return nil, err
}
return &Role{
Identifiable: storableRole.Identifiable,
TimeAuditable: storableRole.TimeAuditable,
DisplayName: storableRole.DisplayName,
Description: storableRole.Description,
OrgID: orgID,
}, nil
}
func NewRole(displayName, description string, orgID valuer.UUID) *Role {
return &Role{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: displayName,
Description: description,
OrgID: orgID,
}
}
func NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.Object, relation authtypes.Relation) (*PatchableObjects, error) {
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")
}
for _, object := range additions {
if !slices.Contains(authtypes.TypeableRelations[object.Resource.Type], relation) {
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeAuthZInvalidRelation, "relation %s is invalid for type %s", relation.StringValue(), object.Resource.Type.StringValue())
}
}
for _, object := range deletions {
if !slices.Contains(authtypes.TypeableRelations[object.Resource.Type], relation) {
return nil, errors.Newf(errors.TypeInvalidInput, authtypes.ErrCodeAuthZInvalidRelation, "relation %s is invalid for type %s", relation.StringValue(), object.Resource.Type.StringValue())
}
}
return &PatchableObjects{Additions: additions, Deletions: deletions}, nil
}
func (role *Role) PatchMetadata(displayName, description *string) {
if displayName != nil {
role.DisplayName = *displayName
}
if description != nil {
role.Description = *description
}
role.UpdatedAt = time.Now()
}
func (role *PostableRole) UnmarshalJSON(data []byte) error {
type shadowPostableRole struct {
DisplayName string `json:"displayName"`
Description string `json:"description"`
}
var shadowRole shadowPostableRole
if err := json.Unmarshal(data, &shadowRole); err != nil {
return err
}
if shadowRole.DisplayName == "" {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "displayName is missing from the request")
}
role.DisplayName = shadowRole.DisplayName
role.Description = shadowRole.Description
return nil
}
func (role *PatchableRole) UnmarshalJSON(data []byte) error {
type shadowPatchableRole struct {
DisplayName *string `json:"displayName"`
Description *string `json:"description"`
}
var shadowRole shadowPatchableRole
if err := json.Unmarshal(data, &shadowRole); err != nil {
return err
}
if shadowRole.DisplayName == nil && shadowRole.Description == nil {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty role patch request received, at least one of displayName or description must be present")
}
role.DisplayName = shadowRole.DisplayName
role.Description = shadowRole.Description
return nil
}
func GetAdditionTuples(id valuer.UUID, relation authtypes.Relation, additions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, object := range additions {
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
authtypes.MustNewSubject(
authtypes.TypeRole,
id.String(),
authtypes.RelationAssignee,
),
relation,
[]authtypes.Selector{object.Selector},
)
if err != nil {
return nil, err
}
tuples = append(tuples, transactionTuples...)
}
return tuples, nil
}
func GetDeletionTuples(id valuer.UUID, relation authtypes.Relation, deletions []*authtypes.Object) ([]*openfgav1.TupleKeyWithoutCondition, error) {
tuples := make([]*openfgav1.TupleKeyWithoutCondition, 0)
for _, object := range deletions {
typeable := authtypes.MustNewTypeableFromType(object.Resource.Type, object.Resource.Name)
transactionTuples, err := typeable.Tuples(
authtypes.MustNewSubject(
authtypes.TypeRole,
id.String(),
authtypes.RelationAssignee,
),
relation,
[]authtypes.Selector{object.Selector},
)
if err != nil {
return nil, err
}
deletionTuples := make([]*openfgav1.TupleKeyWithoutCondition, len(transactionTuples))
for idx, tuple := range transactionTuples {
deletionTuples[idx] = &openfgav1.TupleKeyWithoutCondition{
User: tuple.User,
Relation: tuple.Relation,
Object: tuple.Object,
}
}
tuples = append(tuples, deletionTuples...)
}
return tuples, nil
}

View File

@ -0,0 +1,16 @@
package roletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
List(context.Context, valuer.UUID) ([]*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
}