Compare commits

...

11 Commits

Author SHA1 Message Date
vikrantgupta25
086767b73b feat(authz): convert update membership to patch membership 2025-11-06 17:13:49 +05:30
vikrantgupta25
b185af1aa0 feat(authz): handle membership for role module 2025-11-06 16:36:32 +05:30
vikrantgupta25
03f1541d3e feat(authz): fix lint issue 2025-10-30 22:39:49 +05:30
Vikrant Gupta
ae79796b39 Merge branch 'main' into platform-pod/issues/1136/1 2025-10-30 22:31:34 +05:30
vikrantgupta25
dd3bfda87c feat(authz): add managed role migrations and initializations 2025-10-30 19:53:46 +05:30
vikrantgupta25
84cb046058 feat(authz): fix migration versions 2025-10-30 16:27:22 +05:30
vikrantgupta25
d447b1b03c feat(authz): fix bad rebase issues 2025-10-30 13:23:38 +05:30
vikrantgupta25
a913f52d4d feat(authz): implement marshal text for email 2025-10-30 13:23:38 +05:30
vikrantgupta25
a94a10246f feat(authz): testing changes 2025-10-30 13:23:38 +05:30
vikrantgupta25
ff61712458 feat(authz): add text marshaller for valuer 2025-10-30 13:23:38 +05:30
vikrantgupta25
fab8724ebe feat(authz): add the sqlmigration for authz 2025-10-30 13:23:35 +05:30
34 changed files with 1262 additions and 290 deletions

View File

@@ -8,6 +8,9 @@ import (
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/authz/openfgaauthz"
"github.com/SigNoz/signoz/pkg/authz/openfgaschema"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/licensing/nooplicensing"
@@ -65,6 +68,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return nooplicensing.NewFactory()
},
func(sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),

View File

@@ -8,6 +8,8 @@ import (
"github.com/SigNoz/signoz/cmd"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
@@ -17,6 +19,7 @@ import (
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/organization"
@@ -71,6 +74,9 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
},
func(sqlstore sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))
},
signoz.NewEmailingProviderFactories(),
signoz.NewCacheProviderFactories(),
signoz.NewWebProviderFactories(),

View File

@@ -21,12 +21,12 @@ type role
define update: [user, role#assignee]
define delete: [user, role#assignee]
type resources
type metaresources
relations
define create: [user, role#assignee]
define list: [user, role#assignee]
type resource
type metaresource
relations
define read: [user, anonymous, role#assignee]
define update: [user, role#assignee]
@@ -35,6 +35,6 @@ type resource
define block: [user, role#assignee]
type telemetry
type telemetryresource
relations
define read: [user, anonymous, role#assignee]

View File

@@ -83,15 +83,24 @@ func (ah *APIHandler) Gateway() *httputil.ReverseProxy {
// RegisterRoutes registers routes for this handler on the given router
func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *middleware.AuthZ) {
// note: add ee override methods first
// routes available only in ee version
// v1
router.HandleFunc("/api/v1/features", am.ViewAccess(ah.getFeatureFlags)).Methods(http.MethodGet)
// paid plans specific routes
router.HandleFunc("/api/v1/complete/saml", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionBySAMLCallback)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/complete/oidc", am.OpenAccess(ah.Signoz.Handlers.Session.CreateSessionByOIDCCallback)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles/resources", am.OpenAccess(ah.Signoz.Handlers.Role.GetResources)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles", am.OpenAccess(ah.Signoz.Handlers.Role.List)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles", am.OpenAccess(ah.Signoz.Handlers.Role.Create)).Methods(http.MethodPost)
router.HandleFunc("/api/v1/roles/{id}", am.OpenAccess(ah.Signoz.Handlers.Role.Get)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles/{id}/relation/{relation}/objects", am.OpenAccess(ah.Signoz.Handlers.Role.GetObjects)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles/{id}/membership", am.OpenAccess(ah.Signoz.Handlers.Role.GetMembership)).Methods(http.MethodGet)
router.HandleFunc("/api/v1/roles/{id}", am.OpenAccess(ah.Signoz.Handlers.Role.Patch)).Methods(http.MethodPatch)
router.HandleFunc("/api/v1/roles/{id}/relation/{relation}/objects", am.OpenAccess(ah.Signoz.Handlers.Role.PatchObjects)).Methods(http.MethodPatch)
router.HandleFunc("/api/v1/roles/{id}/membership", am.OpenAccess(ah.Signoz.Handlers.Role.PatchMembership)).Methods(http.MethodPatch)
router.HandleFunc("/api/v1/roles/{id}", am.OpenAccess(ah.Signoz.Handlers.Role.Delete)).Methods(http.MethodDelete)
// base overrides
router.HandleFunc("/api/v1/version", am.OpenAccess(ah.getVersion)).Methods(http.MethodGet)

View File

@@ -177,6 +177,7 @@ func (provider *provider) isModelEqual(expected *openfgav1.AuthorizationModel, a
}
func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKey) error {
provider.mtx.RLock()
checkResponse, err := provider.openfgaServer.Check(
ctx,
&openfgav1.CheckRequest{
@@ -188,6 +189,7 @@ func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKe
Object: tupleReq.Object,
},
})
provider.mtx.RUnlock()
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
}
@@ -211,6 +213,7 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.
})
}
provider.mtx.RLock()
checkResponse, err := provider.openfgaServer.BatchCheck(
ctx,
&openfgav1.BatchCheckRequest{
@@ -221,6 +224,7 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.
if err != nil {
return errors.Newf(errors.TypeInternal, authtypes.ErrCodeAuthZUnavailable, "authorization server is unavailable").WithAdditional(err.Error())
}
provider.mtx.RUnlock()
for _, checkResponse := range checkResponse.Result {
if checkResponse.GetAllowed() {
@@ -257,21 +261,34 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
deletionTuplesWithoutCondition[idx] = &openfgav1.TupleKeyWithoutCondition{User: tuple.User, Object: tuple.Object, Relation: tuple.Relation}
}
provider.mtx.RLock()
_, err := provider.openfgaServer.Write(ctx, &openfgav1.WriteRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: additions,
},
Deletes: &openfgav1.WriteRequestDeletes{
TupleKeys: deletionTuplesWithoutCondition,
},
Writes: func() *openfgav1.WriteRequestWrites {
if len(additions) == 0 {
return nil
}
return &openfgav1.WriteRequestWrites{
TupleKeys: additions,
}
}(),
Deletes: func() *openfgav1.WriteRequestDeletes {
if len(deletionTuplesWithoutCondition) == 0 {
return nil
}
return &openfgav1.WriteRequestDeletes{
TupleKeys: deletionTuplesWithoutCondition,
}
}(),
})
provider.mtx.RUnlock()
return err
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
provider.mtx.RLock()
response, err := provider.openfgaServer.ListObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
@@ -279,6 +296,7 @@ func (provider *provider) ListObjects(ctx context.Context, subject string, relat
Relation: relation.StringValue(),
Type: typeable.Type().StringValue(),
})
provider.mtx.RUnlock()
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())
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
"github.com/SigNoz/signoz/pkg/modules/role"
"github.com/SigNoz/signoz/pkg/types"
)
@@ -13,10 +14,11 @@ type setter struct {
store types.OrganizationStore
alertmanager alertmanager.Alertmanager
quickfilter quickfilter.Module
role role.Module
}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter}
func NewSetter(store types.OrganizationStore, alertmanager alertmanager.Alertmanager, quickfilter quickfilter.Module, role role.Module) organization.Setter {
return &setter{store: store, alertmanager: alertmanager, quickfilter: quickfilter, role: role}
}
func (module *setter) Create(ctx context.Context, organization *types.Organization) error {
@@ -32,6 +34,10 @@ func (module *setter) Create(ctx context.Context, organization *types.Organizati
return err
}
if err := module.role.SetManagedRoles(ctx, organization.ID); err != nil {
return err
}
return nil
}

View File

@@ -17,8 +17,8 @@ type handler struct {
module role.Module
}
func NewHandler(module role.Module) (role.Handler, error) {
return &handler{module: module}, nil
func NewHandler(module role.Module) role.Handler {
return &handler{module: module}
}
func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
@@ -28,25 +28,21 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
req := new(roletypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
if err := binding.JSON.BindBody(r.Body, req, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(rw, err)
return
}
role, err := handler.module.Create(ctx, orgID, req.DisplayName, req.Description)
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())
render.Success(rw, http.StatusCreated, nil)
}
func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
@@ -56,11 +52,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -79,7 +71,22 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
return
}
render.Success(rw, http.StatusOK, role)
membership, err := handler.module.GetMembership(ctx, orgID, roleID)
if err != nil {
render.Error(rw, err)
return
}
userCount := int64(0)
for _, mbshp := range membership {
switch mbshp.Type {
case roletypes.MembershipTypeUser:
userCount++
}
}
gettableRole := roletypes.NewGettableRoleFromRole(role, &roletypes.Attributes{UserCount: userCount})
render.Success(rw, http.StatusOK, gettableRole)
}
func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
@@ -89,11 +96,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -140,40 +143,71 @@ func (handler *handler) GetResources(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusOK, resourceRelations)
}
func (handler *handler) List(rw http.ResponseWriter, r *http.Request) {
func (handler *handler) GetMembership(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
}
memberships, err := handler.module.GetMembership(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, memberships)
}
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 := valuer.MustNewUUID(claims.OrgID)
roles, err := handler.module.List(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
membershipAttributes, err := handler.module.ListMembershipAttributes(ctx, orgID)
if err != nil {
render.Error(rw, err)
return
}
listableRole := make([]*roletypes.GettableRole, len(roles))
for idx, role := range roles {
listableRole[idx] = roletypes.NewGettableRoleFromRole(role, membershipAttributes[role.ID.String()])
}
render.Success(rw, http.StatusOK, listableRole)
}
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 := valuer.MustNewUUID(claims.OrgID)
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -187,7 +221,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
}
req := new(roletypes.PatchableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
if err := binding.JSON.BindBody(r.Body, req, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(rw, err)
return
}
@@ -208,11 +242,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -237,7 +267,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
}
req := new(roletypes.PatchableObjects)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
if err := binding.JSON.BindBody(r.Body, req, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(rw, err)
return
}
@@ -257,6 +287,40 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
render.Success(rw, http.StatusAccepted, nil)
}
func (handler *handler) PatchMembership(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
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.PatchableMembership)
if err := binding.JSON.BindBody(r.Body, req, binding.WithDisallowUnknownFields(true)); err != nil {
render.Error(rw, err)
return
}
err = handler.module.PatchMembership(ctx, valuer.MustNewUUID(claims.OrgID), roleID, req.Additions, req.Deletions)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusNoContent, nil)
}
func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
claims, err := authtypes.ClaimsFromContext(ctx)
@@ -264,11 +328,7 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
render.Error(rw, err)
return
}
orgID, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
orgID := valuer.MustNewUUID(claims.OrgID)
id, ok := mux.Vars(r)["id"]
if !ok {

View File

@@ -5,6 +5,7 @@ import (
"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"
@@ -17,28 +18,22 @@ type module struct {
authz authz.AuthZ
}
func NewModule(ctx context.Context, store roletypes.Store, authz authz.AuthZ, registry []role.RegisterTypeable) (role.Module, error) {
func NewModule(store roletypes.Store, authz authz.AuthZ, registry []role.RegisterTypeable) role.Module {
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)
func (module *module) Create(ctx context.Context, orgID valuer.UUID, displayName, description string) error {
storableRole := roletypes.NewStorableRole(displayName, description, roletypes.RoleTypeCustom, orgID)
err := module.store.Create(ctx, storableRole)
if err != nil {
return nil, err
return err
}
err = module.store.Create(ctx, storableRole)
if err != nil {
return nil, err
}
return role, nil
return nil
}
func (module *module) GetResources(_ context.Context) []*authtypes.Resource {
@@ -46,7 +41,7 @@ func (module *module) GetResources(_ context.Context) []*authtypes.Resource {
for _, register := range module.registry {
typeables = append(typeables, register.MustGetTypeables()...)
}
// role module cannot self register itself!
// role module cannot self register itself hence extracting here.
typeables = append(typeables, module.MustGetTypeables()...)
resources := make([]*authtypes.Resource, 0)
@@ -72,7 +67,7 @@ func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID
}
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)
_, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
@@ -84,7 +79,7 @@ func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valu
authz.
ListObjects(
ctx,
authtypes.MustNewSubject(authtypes.TypeRole, storableRole.ID.String(), authtypes.RelationAssignee),
authtypes.MustNewSubject(authtypes.TypeRole, id.String(), authtypes.RelationAssignee),
relation,
authtypes.MustNewTypeableFromType(resource.Type, resource.Name),
)
@@ -99,6 +94,26 @@ func (module *module) GetObjects(ctx context.Context, orgID valuer.UUID, id valu
return objects, nil
}
func (module *module) GetMembership(ctx context.Context, orgID valuer.UUID, id valuer.UUID) ([]*roletypes.Membership, error) {
_, err := module.store.Get(ctx, orgID, id)
if err != nil {
return nil, err
}
storableMembership, err := module.store.GetMembership(ctx, id)
if err != nil {
return nil, err
}
users, err := module.store.ListUserByRole(ctx, id)
if err != nil {
return nil, err
}
membership := roletypes.NewMembershipFromStorableMembership(storableMembership, users)
return membership, nil
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
storableRoles, err := module.store.List(ctx, orgID)
if err != nil {
@@ -117,6 +132,10 @@ func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes
return roles, nil
}
func (module *module) ListMembershipAttributes(ctx context.Context, orgID valuer.UUID) (map[string]*roletypes.Attributes, error) {
return module.store.ListMembershipAttributes(ctx, orgID)
}
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 {
@@ -128,7 +147,11 @@ func (module *module) Patch(ctx context.Context, orgID valuer.UUID, id valuer.UU
return err
}
role.PatchMetadata(displayName, description)
err = role.PatchMetadata(displayName, description)
if err != nil {
return err
}
updatedRole, err := roletypes.NewStorableRoleFromRole(role)
if err != nil {
return err
@@ -161,10 +184,123 @@ func (module *module) PatchObjects(ctx context.Context, orgID valuer.UUID, id va
return nil
}
func (module *module) PatchMembership(ctx context.Context, orgID valuer.UUID, id valuer.UUID, additions, deletions []*roletypes.UpdatableMembership) error {
_, err := module.store.Get(ctx, orgID, id)
if err != nil {
return err
}
memberships, err := module.store.GetMembership(ctx, id)
if err != nil {
return err
}
updatableMemberships := make([]*roletypes.UpdatableMembership, 0)
membershipTypeAndIDs := make(map[string]bool)
for _, userRole := range memberships.Users {
membershipTypeAndIDs[roletypes.MembershipTypeUser.StringValue()+userRole.UserID] = true
}
deletionMap := make(map[string]bool)
for _, del := range deletions {
switch del.Type {
case roletypes.MembershipTypeUser:
deletionMap[del.Type.StringValue()+del.UserID.StringValue()] = true
}
}
for _, userRole := range memberships.Users {
if !deletionMap[roletypes.MembershipTypeUser.StringValue()+userRole.UserID] {
updatableMemberships = append(updatableMemberships, &roletypes.UpdatableMembership{
Type: roletypes.MembershipTypeUser,
UserID: valuer.MustNewUUID(userRole.UserID),
})
}
}
for _, add := range additions {
switch add.Type {
case roletypes.MembershipTypeUser:
if !membershipTypeAndIDs[add.Type.StringValue()+add.UserID.StringValue()] {
updatableMemberships = append(updatableMemberships, add)
}
}
}
storableMemberships := roletypes.NewStorableMembershipFromUpdatableMemberships(id, updatableMemberships)
err = module.store.UpdateMembership(ctx, id, storableMemberships)
if err != nil {
return err
}
return nil
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
storableRole, err := module.store.Get(ctx, orgID, id)
if err != nil {
return err
}
role, err := roletypes.NewRoleFromStorableRole(storableRole)
if err != nil {
return err
}
if !role.CanEditOrDelete() {
return errors.Newf(errors.TypeInvalidInput, roletypes.ErrCodeRoleInvalidInput, "cannot delete managed role")
}
return module.store.Delete(ctx, orgID, id)
}
func (module *module) SetManagedRoles(ctx context.Context, orgID valuer.UUID) error {
storableManagedRoleAdmin := roletypes.MustNewStorableRole(
roletypes.ManagedRoleSigNozAdminName,
roletypes.ManagedRoleSigNozAdminDescription,
roletypes.RoleTypeManaged,
orgID,
)
storableManagedRoleViewer := roletypes.MustNewStorableRole(
roletypes.ManagedRoleSigNozViewerName,
roletypes.ManagedRoleSigNozViewerDescription,
roletypes.RoleTypeManaged,
orgID,
)
storableManagedRoleEditor := roletypes.MustNewStorableRole(
roletypes.ManagedRoleSigNozEditorName,
roletypes.ManagedRoleSigNozEditorDescription,
roletypes.RoleTypeManaged,
orgID,
)
err := module.store.RunInTx(ctx, func(ctx context.Context) error {
err := module.store.Create(ctx, storableManagedRoleAdmin)
if err != nil {
return err
}
err = module.store.Create(ctx, storableManagedRoleViewer)
if err != nil {
return err
}
err = module.store.Create(ctx, storableManagedRoleEditor)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
func (module *module) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
}

View File

@@ -2,9 +2,11 @@ package implrole
import (
"context"
"database/sql"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -13,14 +15,14 @@ type store struct {
sqlstore sqlstore.SQLStore
}
func NewStore(sqlstore sqlstore.SQLStore) (roletypes.Store, error) {
return &store{sqlstore: sqlstore}, nil
func NewStore(sqlstore sqlstore.SQLStore) roletypes.Store {
return &store{sqlstore: sqlstore}
}
func (store *store) Create(ctx context.Context, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewInsert().
Model(role).
Exec(ctx)
@@ -35,10 +37,10 @@ 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("orgID = ?", orgID).
Where("org_id = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
@@ -48,26 +50,103 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
return role, nil
}
func (store *store) GetMembership(ctx context.Context, id valuer.UUID) (*roletypes.StorableMembership, error) {
storableUserMembership := make([]*roletypes.StorableUserRole, 0)
err := store.RunInTx(ctx, func(ctx context.Context) error {
err := store.sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&storableUserMembership).
Relation("User").
Where("role_id = ?", id).
Scan(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return roletypes.MakeStorableMembership(storableUserMembership)
}
func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.StorableRole, error) {
roles := make([]*roletypes.StorableRole, 0)
err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewSelect().
Model(&roles).
Where("orgID = ?", orgID).
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) ListMembershipAttributes(ctx context.Context, orgID valuer.UUID) (map[string]*roletypes.Attributes, error) {
type resultRow struct {
RoleID string `bun:"role_id"`
UserCount int64 `bun:"user_count"`
}
var rows []resultRow
// Count users per role for the org
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model((*roletypes.StorableUserRole)(nil)).
Column("role_id").
ColumnExpr("count(*) as user_count").
Join("JOIN role ON role.id = user_role.role_id").
Where("role.org_id = ?", orgID).
Group("role_id").
Scan(ctx, &rows)
if err != nil {
return nil, err
}
// Map of role_id -> Attributes{UserCount}
attrMap := make(map[string]*roletypes.Attributes, len(rows))
for _, row := range rows {
attrMap[row.RoleID] = &roletypes.Attributes{
UserCount: row.UserCount,
}
}
return attrMap, nil
}
func (store *store) ListUserByRole(ctx context.Context, id valuer.UUID) ([]*types.User, error) {
users := make([]*types.User, 0)
err := store.
sqlstore.
BunDBCtx(ctx).
NewSelect().
Model(&users).
Join("user_role").
JoinOn("users.id = user_role.user_id").
Where("user_role.role_id = ?", id).
Scan(ctx)
if err != nil {
return nil, err
}
return users, nil
}
func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletypes.StorableRole) error {
_, err := store.
sqlstore.
BunDB().
BunDBCtx(ctx).
NewUpdate().
Model(role).
WherePK().
@@ -80,10 +159,39 @@ func (store *store) Update(ctx context.Context, orgID valuer.UUID, role *roletyp
return nil
}
func (store *store) UpdateMembership(ctx context.Context, id valuer.UUID, storableMembership *roletypes.StorableMembership) error {
err := store.RunInTx(ctx, func(ctx context.Context) error {
_, err := store.sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(roletypes.StorableUserRole)).
Where("role_id = ?", id).
Exec(ctx)
if err != nil {
return err
}
_, err = store.sqlstore.
BunDBCtx(ctx).
NewInsert().
Model(&storableMembership.Users).
Exec(ctx)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
return nil
}
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).
@@ -96,6 +204,21 @@ func (store *store) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUI
return nil
}
func (store *store) DeleteMembership(ctx context.Context, id valuer.UUID) error {
_, err := store.
sqlstore.
BunDBCtx(ctx).
NewDelete().
Model(new(roletypes.StorableUserRole)).
Where("role_id = ?", id).
Exec(ctx)
if err != nil && err != sql.ErrNoRows {
return err
}
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)

View File

@@ -11,7 +11,7 @@ import (
type Module interface {
// Creates the role metadata
Create(context.Context, valuer.UUID, string, string) (*roletypes.Role, error)
Create(context.Context, valuer.UUID, string, string) error
// Gets the role metadata
Get(context.Context, valuer.UUID, valuer.UUID) (*roletypes.Role, error)
@@ -19,9 +19,14 @@ type Module interface {
// Gets the objects associated with the given role and relation
GetObjects(context.Context, valuer.UUID, valuer.UUID, authtypes.Relation) ([]*authtypes.Object, error)
// Gets the membership for the given role
GetMembership(context.Context, valuer.UUID, valuer.UUID) ([]*roletypes.Membership, error)
// Lists all the roles metadata for the organization
List(context.Context, valuer.UUID) ([]*roletypes.Role, error)
ListMembershipAttributes(context.Context, valuer.UUID) (map[string]*roletypes.Attributes, error)
// Gets all the typeable resources registered from role registry
GetResources(context.Context) []*authtypes.Resource
@@ -31,9 +36,15 @@ type Module interface {
// 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
// Patches the membership for the given role
PatchMembership(context.Context, valuer.UUID, valuer.UUID, []*roletypes.UpdatableMembership, []*roletypes.UpdatableMembership) error
// Deletes the role metadata and tuples in authorization server
Delete(context.Context, valuer.UUID, valuer.UUID) error
// Initializes the managed roles on creation of the organization
SetManagedRoles(context.Context, valuer.UUID) error
RegisterTypeable
}
@@ -50,11 +61,15 @@ type Handler interface {
GetResources(http.ResponseWriter, *http.Request)
GetMembership(http.ResponseWriter, *http.Request)
List(http.ResponseWriter, *http.Request)
Patch(http.ResponseWriter, *http.Request)
PatchObjects(http.ResponseWriter, *http.Request)
PatchMembership(http.ResponseWriter, *http.Request)
Delete(http.ResponseWriter, *http.Request)
}

View File

@@ -16,6 +16,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/session"
@@ -41,6 +43,7 @@ type Handlers struct {
AuthDomain authdomain.Handler
Session session.Handler
SpanPercentile spanpercentile.Handler
Role role.Handler
}
func NewHandlers(modules Modules, providerSettings factory.ProviderSettings) Handlers {
@@ -57,5 +60,6 @@ func NewHandlers(modules Modules, providerSettings factory.ProviderSettings) Han
AuthDomain: implauthdomain.NewHandler(modules.AuthDomain),
Session: implsession.NewHandler(modules.Session),
SpanPercentile: implspanpercentile.NewHandler(modules.SpanPercentile),
Role: implrole.NewHandler(modules.Role),
}
}

View File

@@ -35,7 +35,7 @@ func TestNewHandlers(t *testing.T) {
require.NoError(t, err)
tokenizer := tokenizertest.New()
emailing := emailingtest.New()
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil)
handlers := NewHandlers(modules, providerSettings)

View File

@@ -4,6 +4,7 @@ import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/modules/apdex"
@@ -20,6 +21,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/session"
@@ -52,6 +55,7 @@ type Modules struct {
AuthDomain authdomain.Module
Session session.Module
SpanPercentile spanpercentile.Module
Role role.Module
}
func NewModules(
@@ -64,19 +68,21 @@ func NewModules(
analytics analytics.Analytics,
querier querier.Querier,
authNs map[authtypes.AuthNProvider]authn.AuthN,
authz authz.AuthZ,
) Modules {
quickfilter := implquickfilter.NewModule(implquickfilter.NewStore(sqlstore))
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter)
dashboard := impldashboard.NewModule(sqlstore, providerSettings, analytics)
role := implrole.NewModule(implrole.NewStore(sqlstore), authz, []role.RegisterTypeable{dashboard})
orgSetter := implorganization.NewSetter(implorganization.NewStore(sqlstore), alertmanager, quickfilter, role)
user := impluser.NewModule(impluser.NewStore(sqlstore, providerSettings), tokenizer, emailing, providerSettings, orgSetter, analytics)
userGetter := impluser.NewGetter(impluser.NewStore(sqlstore, providerSettings))
return Modules{
OrgGetter: orgGetter,
OrgSetter: orgSetter,
Preference: implpreference.NewModule(implpreference.NewStore(sqlstore), preferencetypes.NewAvailablePreference()),
SavedView: implsavedview.NewModule(sqlstore),
Apdex: implapdex.NewModule(sqlstore),
Dashboard: impldashboard.NewModule(sqlstore, providerSettings, analytics),
Dashboard: dashboard,
User: user,
UserGetter: userGetter,
QuickFilter: quickfilter,
@@ -85,5 +91,6 @@ func NewModules(
AuthDomain: implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)),
Session: implsession.NewModule(providerSettings, authNs, user, userGetter, implauthdomain.NewModule(implauthdomain.NewStore(sqlstore)), tokenizer, orgGetter),
SpanPercentile: implspanpercentile.NewModule(querier, providerSettings),
Role: role,
}
}

View File

@@ -35,7 +35,7 @@ func TestNewModules(t *testing.T) {
require.NoError(t, err)
tokenizer := tokenizertest.New()
emailing := emailingtest.New()
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, nil, nil, nil, nil)
reflectVal := reflect.ValueOf(modules)
for i := 0; i < reflectVal.NumField(); i++ {

View File

@@ -138,6 +138,8 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewUpdateTTLSettingForCustomRetentionFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoutePolicyFactory(sqlstore, sqlschema),
sqlmigration.NewAddAuthTokenFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoleFactory(sqlstore, sqlschema),
sqlmigration.NewOpenfgaMigrationFactory(sqlstore, sqlschema),
)
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/analytics"
"github.com/SigNoz/signoz/pkg/authn"
"github.com/SigNoz/signoz/pkg/authn/authnstore/sqlauthnstore"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/emailing"
"github.com/SigNoz/signoz/pkg/factory"
@@ -62,6 +63,7 @@ func New(
zeusProviderFactory factory.ProviderFactory[zeus.Zeus, zeus.Config],
licenseConfig licensing.Config,
licenseProviderFactory func(sqlstore.SQLStore, zeus.Zeus, organization.Getter, analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config],
authzProviderFactoryCb func(sqlstore.SQLStore) factory.ProviderFactory[authz.AuthZ, authz.Config],
emailingProviderFactories factory.NamedMap[factory.ProviderFactory[emailing.Emailing, emailing.Config]],
cacheProviderFactories factory.NamedMap[factory.ProviderFactory[cache.Cache, cache.Config]],
webProviderFactories factory.NamedMap[factory.ProviderFactory[web.Web, web.Config]],
@@ -299,8 +301,15 @@ func New(
return nil, err
}
// Initialize authz
authzProviderFactory := authzProviderFactoryCb(sqlstore)
authz, err := authzProviderFactory.New(ctx, providerSettings, authz.Config{})
if err != nil {
return nil, err
}
// Initialize all modules
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, authNs)
modules := NewModules(sqlstore, tokenizer, emailing, providerSettings, orgGetter, alertmanager, analytics, querier, authNs, authz)
// Initialize all handlers for the modules
handlers := NewHandlers(modules, providerSettings)
@@ -337,6 +346,7 @@ func New(
factory.NewNamedService(factory.MustNewName("licensing"), licensing),
factory.NewNamedService(factory.MustNewName("statsreporter"), statsReporter),
factory.NewNamedService(factory.MustNewName("tokenizer"), tokenizer),
factory.NewNamedService(factory.MustNewName("authz"), authz),
)
if err != nil {
return nil, err

View File

@@ -0,0 +1,193 @@
package sqlmigration
import (
"context"
"database/sql"
"time"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type addRole struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddRoleFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_role"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newAddRole(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newAddRole(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addRole{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *addRole) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addRole) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "role",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "display_name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "description", DataType: sqlschema.DataTypeText, Nullable: true},
{Name: "type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "org_id", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("org_id"),
ReferencedTableName: sqlschema.TableName("organizations"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tableSQLs...)
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "role", ColumnNames: []sqlschema.ColumnName{"display_name", "org_id"}})
sqls = append(sqls, indexSQLs...)
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "user_role",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "role_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_id", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
ForeignKeyConstraints: []*sqlschema.ForeignKeyConstraint{
{
ReferencingColumnName: sqlschema.ColumnName("role_id"),
ReferencedTableName: sqlschema.TableName("role"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
{
ReferencingColumnName: sqlschema.ColumnName("user_id"),
ReferencedTableName: sqlschema.TableName("users"),
ReferencedColumnName: sqlschema.ColumnName("id"),
},
},
})
sqls = append(sqls, tableSQLs...)
indexSQLs = migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "user_role", ColumnNames: []sqlschema.ColumnName{"role_id", "user_id"}})
sqls = append(sqls, indexSQLs...)
for _, sqlStmt := range sqls {
if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil {
return err
}
}
var orgIDs []string
err = tx.NewSelect().
Table("organizations").
Column("id").
Scan(ctx, &orgIDs)
if err != nil && err != sql.ErrNoRows {
return err
}
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"`
Type string `bun:"type,type:string"`
OrgID string `bun:"org_id,type:string"`
}
for _, orgID := range orgIDs {
roles := []storableRole{
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: "SigNoz Admin",
Description: "Default SigNoz Admin with all the permissions",
Type: "managed",
OrgID: orgID,
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: "SigNoz Editor",
Description: "Default SigNoz Editor with certain write permission",
Type: "managed",
OrgID: orgID,
},
{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: "SigNoz Viewer",
Description: "Org member role with permissions to view and collaborate",
Type: "managed",
OrgID: orgID,
},
}
if _, err := tx.NewInsert().
Model(&roles).
Exec(ctx); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *addRole) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -0,0 +1,159 @@
package sqlmigration
import (
"context"
"github.com/SigNoz/signoz/pkg/factory"
"github.com/SigNoz/signoz/pkg/sqlschema"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/uptrace/bun"
"github.com/uptrace/bun/migrate"
)
type openfgaMigraion struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewOpenfgaMigrationFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("openfga_migration"), func(ctx context.Context, providerSettings factory.ProviderSettings, config Config) (SQLMigration, error) {
return newOpenfgaMigration(ctx, providerSettings, config, sqlstore, sqlschema)
})
}
func newOpenfgaMigration(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &openfgaMigraion{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *openfgaMigraion) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *openfgaMigraion) Up(ctx context.Context, db *bun.DB) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
_ = tx.Rollback()
}()
sqls := [][]byte{}
tableSQLs := migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "tuple",
Columns: []*sqlschema.Column{
{Name: "store", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "object_type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "object_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_object_type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "relation", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_object_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_relation", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "ulid", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "inserted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "condition_name", DataType: sqlschema.DataTypeText, Nullable: true},
//todo check why openfga uses long blob for this
{Name: "condition_context", DataType: sqlschema.DataTypeText, Nullable: true},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"store", "object_type", "object_id", "relation", "user_object_type", "user_object_id", "user_relation"},
},
})
sqls = append(sqls, tableSQLs...)
indexSQLs := migration.sqlschema.Operator().CreateIndex(&sqlschema.UniqueIndex{TableName: "tuple", ColumnNames: []sqlschema.ColumnName{"ulid"}})
sqls = append(sqls, indexSQLs...)
// todo: need to create this indexes
// CREATE INDEX idx_reverse_lookup_user ON tuple (store, object_type, relation, user_object_type, user_object_id, user_relation);
// CREATE INDEX idx_tuple_partial_user ON tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) WHERE user_type = 'user';
// CREATE INDEX idx_tuple_partial_userset ON tuple (store, object_type, object_id, relation, user_object_type, user_object_id, user_relation) WHERE user_type = 'userset';
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "authorization_model",
Columns: []*sqlschema.Column{
{Name: "store", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "authorization_model_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "schema_version", DataType: sqlschema.DataTypeText, Nullable: false, Default: "1.1"},
{Name: "serialized_protobuf", DataType: sqlschema.DataTypeText, Nullable: false},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"store", "authorization_model_id"},
},
})
sqls = append(sqls, tableSQLs...)
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "store",
Columns: []*sqlschema.Column{
{Name: "id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "name", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "created_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "updated_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
{Name: "deleted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: true},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"id"},
},
})
sqls = append(sqls, tableSQLs...)
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "assertion",
Columns: []*sqlschema.Column{
{Name: "store", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "authorization_model_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "assertions", DataType: sqlschema.DataTypeText, Nullable: true},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"store", "authorization_model_id"},
},
})
sqls = append(sqls, tableSQLs...)
tableSQLs = migration.sqlschema.Operator().CreateTable(&sqlschema.Table{
Name: "changelog",
Columns: []*sqlschema.Column{
{Name: "store", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "object_type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "object_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "relation", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_object_type", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_object_id", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "user_relation", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "operation", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "ulid", DataType: sqlschema.DataTypeText, Nullable: false},
{Name: "inserted_at", DataType: sqlschema.DataTypeTimestamp, Nullable: false},
{Name: "condition_name", DataType: sqlschema.DataTypeText, Nullable: true},
{Name: "condition_context", DataType: sqlschema.DataTypeText, Nullable: true},
},
PrimaryKeyConstraint: &sqlschema.PrimaryKeyConstraint{
ColumnNames: []sqlschema.ColumnName{"store", "object_type", "ulid"},
},
})
sqls = append(sqls, tableSQLs...)
for _, sqlStmt := range sqls {
if _, err := tx.ExecContext(ctx, string(sqlStmt)); err != nil {
return err
}
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func (migration *openfgaMigraion) Down(ctx context.Context, db *bun.DB) error {
return nil
}

View File

@@ -11,6 +11,11 @@ var (
nameRegex = regexp.MustCompile("^[a-z]{1,35}$")
)
var (
_ json.Marshaler = new(Name)
_ json.Unmarshaler = new(Name)
)
type Name struct {
val string
}
@@ -36,6 +41,10 @@ func (name Name) String() string {
return name.val
}
func (name *Name) MarshalJSON() ([]byte, error) {
return json.Marshal(name.val)
}
func (name *Name) UnmarshalJSON(data []byte) error {
nameStr := ""
err := json.Unmarshal(data, &nameStr)

View File

@@ -20,11 +20,11 @@ var (
)
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},
TypeUser: {RelationRead, RelationUpdate, RelationDelete},
TypeRole: {RelationAssignee, RelationRead, RelationUpdate, RelationDelete},
TypeOrganization: {RelationCreate, RelationRead, RelationUpdate, RelationDelete, RelationList},
TypeMetaResource: {RelationRead, RelationUpdate, RelationDelete, RelationBlock},
TypeMetaResources: {RelationCreate, RelationList},
}
type Relation struct{ valuer.String }

View File

@@ -9,16 +9,21 @@ import (
)
var (
ErrCodeAuthZInvalidSelectorRegex = errors.MustNewCode("authz_invalid_selector_regex")
ErrCodeAuthZInvalidSelector = errors.MustNewCode("authz_invalid_selector")
)
var (
_ json.Marshaler = new(Selector)
_ json.Unmarshaler = new(Selector)
)
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}$`)
// resources selectors are used to select either all or none
typeResourcesSelectorRegex = regexp.MustCompile(`^\*$`)
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(`^\*$`)
)
type SelectorCallbackFn func(context.Context, Claims) ([]Selector, error)
@@ -36,33 +41,6 @@ func NewSelector(typed Type, selector string) (Selector, error) {
return Selector{val: selector}, nil
}
func IsValidSelector(typed Type, selector string) error {
switch typed {
case TypeUser:
if !typeUserSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeUserSelectorRegex.String())
}
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeRoleSelectorRegex.String())
}
case TypeOrganization:
if !typeOrganizationSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeOrganizationSelectorRegex.String())
}
case TypeResource:
if !typeResourceSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelectorRegex, "selector must conform to regex %s", typeResourceSelectorRegex.String())
}
case TypeResources:
if !typeResourcesSelectorRegex.MatchString(selector) {
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 {
@@ -76,6 +54,10 @@ func (selector Selector) String() string {
return selector.val
}
func (selector *Selector) MarshalJSON() ([]byte, error) {
return json.Marshal(selector.val)
}
func (typed *Selector) UnmarshalJSON(data []byte) error {
str := ""
err := json.Unmarshal(data, &str)
@@ -83,8 +65,35 @@ func (typed *Selector) UnmarshalJSON(data []byte) error {
return err
}
shadow := Selector{val: str}
*typed = shadow
alias := Selector{val: str}
*typed = alias
return nil
}
func IsValidSelector(typed Type, selector string) error {
switch typed {
case TypeUser:
if !typeUserSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeUserSelectorRegex.String())
}
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String())
}
case TypeOrganization:
if !typeOrganizationSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeOrganizationSelectorRegex.String())
}
case TypeMetaResource:
if !typeMetaResourceSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeMetaResourceSelectorRegex.String())
}
case TypeMetaResources:
if !typeMetaResourcesSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeMetaResourcesSelectorRegex.String())
}
}
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidType, "invalid type: %s", typed)
}

View File

@@ -33,22 +33,24 @@ func NewObject(resource Resource, selector Selector) (*Object, error) {
}
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))
parts := strings.Split(input, "/")
if len(parts) != 4 {
panic(errors.Newf(errors.TypeInternal, errors.CodeInternal, "invalid input format: %s", input))
}
typeParts := strings.Split(parts[0], ":")
if len(typeParts) != 2 {
panic(errors.Newf(errors.TypeInternal, errors.CodeInternal, "invalid type format: %s", parts[0]))
}
resource := Resource{
Type: MustNewType(parts[0]),
Name: MustNewName(parts[1]),
Type: MustNewType(typeParts[0]),
Name: MustNewName(parts[2]),
}
object := &Object{
Resource: resource,
Selector: MustNewSelector(resource.Type, parts[2]),
}
selector := MustNewSelector(resource.Type, parts[3])
return object
return &Object{Resource: resource, Selector: selector}
}
func MustNewObjectsFromStringSlice(input []string) []*Object {
@@ -59,6 +61,14 @@ func MustNewObjectsFromStringSlice(input []string) []*Object {
return objects
}
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 (object *Object) UnmarshalJSON(data []byte) error {
var shadow = struct {
Resource Resource
@@ -79,14 +89,6 @@ func (object *Object) UnmarshalJSON(data []byte) error {
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

View File

@@ -11,14 +11,15 @@ import (
var (
ErrCodeAuthZUnavailable = errors.MustNewCode("authz_unavailable")
ErrCodeAuthZForbidden = errors.MustNewCode("authz_forbidden")
ErrCodeAuthZInvalidType = errors.MustNewCode("authz_invalid_type")
)
var (
TypeUser = Type{valuer.NewString("user")}
TypeRole = Type{valuer.NewString("role")}
TypeOrganization = Type{valuer.NewString("organization")}
TypeResource = Type{valuer.NewString("resource")}
TypeResources = Type{valuer.NewString("resources")}
TypeUser = Type{valuer.NewString("user")}
TypeRole = Type{valuer.NewString("role")}
TypeOrganization = Type{valuer.NewString("organization")}
TypeMetaResource = Type{valuer.NewString("metaresource")}
TypeMetaResources = Type{valuer.NewString("metaresources")}
)
var (
@@ -53,12 +54,12 @@ func NewType(input string) (Type, error) {
return TypeRole, nil
case "organization":
return TypeOrganization, nil
case "resource":
return TypeResource, nil
case "resources":
return TypeResources, nil
case "metaresource":
return TypeMetaResource, nil
case "metaresources":
return TypeMetaResources, nil
default:
return Type{}, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid type: %s", input)
return Type{}, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidType, "invalid type: %s", input)
}
}
@@ -69,12 +70,12 @@ func (typed *Type) UnmarshalJSON(data []byte) error {
return err
}
shadow, err := NewType(str)
alias, err := NewType(str)
if err != nil {
return err
}
*typed = shadow
*typed = alias
return nil
}
@@ -86,21 +87,21 @@ func NewTypeableFromType(typed Type, name Name) (Typeable, error) {
return TypeableUser, nil
case TypeOrganization:
return TypeableOrganization, nil
case TypeResource:
resource, err := NewTypeableResource(name)
case TypeMetaResource:
resource, err := NewTypeableMetaResource(name)
if err != nil {
return nil, err
}
return resource, nil
case TypeResources:
resources, err := NewTypeableResources(name)
case TypeMetaResources:
resources, err := NewTypeableMetaResources(name)
if err != nil {
return nil, err
}
return resources, nil
}
return nil, errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid type")
return nil, errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidType, "invalid type %s", typed)
}
func MustNewTypeableFromType(typed Type, name Name) Typeable {

View File

@@ -0,0 +1,47 @@
package authtypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableMetaResource)
type typeableMetaResource struct {
name Name
}
func NewTypeableMetaResource(name Name) (Typeable, error) {
return &typeableMetaResource{name: name}, nil
}
func MustNewTypeableMetaResource(name Name) Typeable {
typeableesource, err := NewTypeableMetaResource(name)
if err != nil {
panic(err)
}
return typeableesource
}
func (typeableMetaResource *typeableMetaResource) Tuples(subject string, relation Relation, selector []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableMetaResource.Prefix(orgID) + "/" + selector.String()
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableMetaResource *typeableMetaResource) Type() Type {
return TypeMetaResource
}
func (typeableMetaResource *typeableMetaResource) Name() Name {
return typeableMetaResource.name
}
func (typeableMetaResource *typeableMetaResource) Prefix(orgID valuer.UUID) string {
// example: metaresource:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboard
return typeableMetaResource.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableMetaResource.Name().String()
}

View File

@@ -0,0 +1,47 @@
package authtypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
)
var _ Typeable = new(typeableMetaResources)
type typeableMetaResources struct {
name Name
}
func NewTypeableMetaResources(name Name) (Typeable, error) {
return &typeableMetaResources{name: name}, nil
}
func MustNewTypeableMetaResources(name Name) Typeable {
resources, err := NewTypeableMetaResources(name)
if err != nil {
panic(err)
}
return resources
}
func (typeableResources *typeableMetaResources) Tuples(subject string, relation Relation, selector []Selector, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableResources.Prefix(orgID) + "/" + selector.String()
tuples = append(tuples, &openfgav1.TupleKey{User: subject, Relation: relation.StringValue(), Object: object})
}
return tuples, nil
}
func (typeableMetaResources *typeableMetaResources) Type() Type {
return TypeMetaResources
}
func (typeableMetaResources *typeableMetaResources) Name() Name {
return typeableMetaResources.name
}
func (typeableMetaResources *typeableMetaResources) Prefix(orgID valuer.UUID) string {
// example: metaresources:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboards
return typeableMetaResources.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableMetaResources.Name().String()
}

View File

@@ -1,47 +0,0 @@
package authtypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
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, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableResource.Prefix(orgID) + "/" + 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(orgID valuer.UUID) string {
// example: resource:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboard
return typeableResource.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableResource.Name().String()
}

View File

@@ -1,47 +0,0 @@
package authtypes
import (
"github.com/SigNoz/signoz/pkg/valuer"
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, orgID valuer.UUID) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)
for _, selector := range selector {
object := typeableResources.Prefix(orgID) + "/" + 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(orgID valuer.UUID) string {
// example: resources:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboards
return typeableResources.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableResources.Name().String()
}

View File

@@ -13,8 +13,8 @@ import (
)
var (
TypeableResourceDashboard = authtypes.MustNewTypeableResource(authtypes.MustNewName("dashboard"))
TypeableResourcesDashboards = authtypes.MustNewTypeableResources(authtypes.MustNewName("dashboards"))
TypeableResourceDashboard = authtypes.MustNewTypeableMetaResource(authtypes.MustNewName("dashboard"))
TypeableResourcesDashboards = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("dashboards"))
)
type StorableDashboard struct {

View File

@@ -19,10 +19,29 @@ var (
ErrCodeInvalidTypeRelation = errors.MustNewCode("role_invalid_type_relation")
ErrCodeRoleNotFound = errors.MustNewCode("role_not_found")
ErrCodeRoleFailedTransactionsFromString = errors.MustNewCode("role_failed_transactions_from_string")
ErrCodeRoleInvalidMembershipType = errors.MustNewCode("role_invalid_membership_type")
)
var (
TypeableResourcesRoles = authtypes.MustNewTypeableResources(authtypes.MustNewName("roles"))
MembershipTypeUser = valuer.NewString("user")
)
var (
TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles"))
)
var (
RoleTypeManaged = valuer.NewString("managed")
RoleTypeCustom = valuer.NewString("custom")
ManagedRoleSigNozAdminName = "SigNoz Admin"
ManagedRoleSigNozAdminDescription = "Default SigNoz Admin with all the permissions"
ManagedRoleSigNozEditorName = "SigNoz Editor"
ManagedRoleSigNozEditorDescription = "Default SigNoz Editor with certain write permission"
ManagedRoleSigNozViewerName = "SigNoz Viewer"
ManagedRoleSigNozViewerDescription = "Org member role with permissions to view and collaborate"
)
type StorableRole struct {
@@ -32,15 +51,52 @@ type StorableRole struct {
types.TimeAuditable
DisplayName string `bun:"display_name,type:string"`
Description string `bun:"description,type:string"`
Type string `bun:"type,type:boolean"`
OrgID string `bun:"org_id,type:string"`
}
type StorableUserRole struct {
bun.BaseModel `bun:"table:user_role"`
types.Identifiable
RoleID string `bun:"role_id,type:text"`
UserID string `bun:"user_id,type:text"`
}
type StorableMembership struct {
Users []*StorableUserRole
}
type Role struct {
types.Identifiable
types.TimeAuditable
DisplayName string `json:"displayName"`
Description string `json:"description"`
OrgID valuer.UUID `json:"org_id"`
Type string `json:"type"`
OrgID valuer.UUID `json:"orgId"`
}
type GettableRole struct {
types.Identifiable
types.TimeAuditable
DisplayName string `json:"displayName"`
Description string `json:"description"`
Type string `json:"type"`
OrgID valuer.UUID `json:"orgId"`
Attributes *Attributes `json:"attributes"`
}
type Attributes struct {
UserCount int64 `json:"user_count"`
}
type Membership struct {
Type valuer.String `json:"type"`
User *types.User `json:"user"`
}
type UpdatableMembership struct {
Type valuer.String `json:"type"`
UserID valuer.UUID `json:"userId"`
}
type PostableRole struct {
@@ -58,32 +114,66 @@ type PatchableObjects struct {
Deletions []*authtypes.Object `json:"deletions"`
}
type PatchableMembership struct {
Additions []*UpdatableMembership `json:"additions"`
Deletions []*UpdatableMembership `json:"deletions"`
}
func NewStorableRole(displayName, description string, roleType valuer.String, orgID valuer.UUID) *StorableRole {
return &StorableRole{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: displayName,
Description: description,
Type: roleType.StringValue(),
OrgID: orgID.StringValue(),
}
}
func MustNewStorableRole(displayName, description string, roleType valuer.String, orgID valuer.UUID) *StorableRole {
return &StorableRole{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
TimeAuditable: types.TimeAuditable{
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
DisplayName: displayName,
Description: description,
Type: roleType.StringValue(),
OrgID: orgID.StringValue(),
}
}
func NewStorableRoleFromRole(role *Role) (*StorableRole, error) {
return &StorableRole{
Identifiable: role.Identifiable,
TimeAuditable: role.TimeAuditable,
DisplayName: role.DisplayName,
Description: role.Description,
Type: role.Type,
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,
Type: storableRole.Type,
OrgID: valuer.MustNewUUID(storableRole.OrgID),
}, nil
}
func NewRole(displayName, description string, orgID valuer.UUID) *Role {
func NewRole(displayName, description string, roleType valuer.String, orgID valuer.UUID) *Role {
return &Role{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
@@ -94,13 +184,27 @@ func NewRole(displayName, description string, orgID valuer.UUID) *Role {
},
DisplayName: displayName,
Description: description,
Type: roleType.StringValue(),
OrgID: orgID,
}
}
func NewGettableRoleFromRole(role *Role, attributes *Attributes) *GettableRole {
return &GettableRole{
Identifiable: role.Identifiable,
TimeAuditable: role.TimeAuditable,
DisplayName: role.DisplayName,
Description: role.Description,
Type: role.Type,
OrgID: role.OrgID,
Attributes: attributes,
}
}
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")
return nil, errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty patch objects request received, at least one of additions or deletions must be present")
}
for _, object := range additions {
@@ -118,7 +222,53 @@ func NewPatchableObjects(additions []*authtypes.Object, deletions []*authtypes.O
return &PatchableObjects{Additions: additions, Deletions: deletions}, nil
}
func (role *Role) PatchMetadata(displayName, description *string) {
func NewMembershipFromStorableMembership(storableMembership *StorableMembership, users []*types.User) []*Membership {
usersMap := make(map[string]*types.User)
for _, user := range users {
usersMap[user.ID.String()] = user
}
membership := make([]*Membership, 0)
for _, userRole := range storableMembership.Users {
membership = append(membership, &Membership{
Type: MembershipTypeUser,
User: usersMap[userRole.UserID],
})
}
return membership
}
func NewStorableMembershipFromUpdatableMemberships(id valuer.UUID, updatableMemberships []*UpdatableMembership) *StorableMembership {
userMemberships := make([]*StorableUserRole, 0)
for _, membership := range updatableMemberships {
switch membership.Type {
case MembershipTypeUser:
userMemberships = append(userMemberships, &StorableUserRole{
Identifiable: types.Identifiable{
ID: valuer.GenerateUUID(),
},
UserID: membership.UserID.StringValue(),
RoleID: id.StringValue(),
})
}
}
return &StorableMembership{Users: userMemberships}
}
func MakeStorableMembership(users []*StorableUserRole) (*StorableMembership, error) {
storableMembership := new(StorableMembership)
storableMembership.Users = users
return storableMembership, nil
}
func (role *Role) PatchMetadata(displayName, description *string) error {
if !role.CanEditOrDelete() {
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "cannot patch managed roles")
}
if displayName != nil {
role.DisplayName = *displayName
}
@@ -126,6 +276,12 @@ func (role *Role) PatchMetadata(displayName, description *string) {
role.Description = *description
}
role.UpdatedAt = time.Now()
return nil
}
func (role *Role) CanEditOrDelete() bool {
return role.Type == RoleTypeCustom.StringValue()
}
func (role *PostableRole) UnmarshalJSON(data []byte) error {
@@ -161,7 +317,11 @@ func (role *PatchableRole) UnmarshalJSON(data []byte) error {
}
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")
return errors.New(errors.TypeInvalidInput, ErrCodeRoleEmptyPatch, "empty patch role request received, at least one of displayName or description must be present")
}
if shadowRole.DisplayName != nil && *shadowRole.DisplayName == "" {
return errors.New(errors.TypeInvalidInput, ErrCodeRoleInvalidInput, "cannot set empty displayName for the role")
}
role.DisplayName = shadowRole.DisplayName
@@ -170,6 +330,22 @@ func (role *PatchableRole) UnmarshalJSON(data []byte) error {
return nil
}
func (request *UpdatableMembership) UnmarshalJSON(data []byte) error {
type Alias UpdatableMembership
var temp Alias
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
if temp.Type != MembershipTypeUser {
return errors.Newf(errors.TypeInvalidInput, ErrCodeRoleInvalidMembershipType, "invalid membership type, accepted values are :%s", MembershipTypeUser)
}
*request = UpdatableMembership(temp)
return nil
}
func GetAdditionTuples(id valuer.UUID, orgID valuer.UUID, relation authtypes.Relation, additions []*authtypes.Object) ([]*openfgav1.TupleKey, error) {
tuples := make([]*openfgav1.TupleKey, 0)

View File

@@ -3,14 +3,20 @@ package roletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
Create(context.Context, *StorableRole) error
Get(context.Context, valuer.UUID, valuer.UUID) (*StorableRole, error)
GetMembership(context.Context, valuer.UUID) (*StorableMembership, error)
List(context.Context, valuer.UUID) ([]*StorableRole, error)
ListMembershipAttributes(context.Context, valuer.UUID) (map[string]*Attributes, error)
ListUserByRole(context.Context, valuer.UUID) ([]*types.User, error)
Update(context.Context, valuer.UUID, *StorableRole) error
UpdateMembership(context.Context, valuer.UUID, *StorableMembership) error
Delete(context.Context, valuer.UUID, valuer.UUID) error
DeleteMembership(context.Context, valuer.UUID) error
RunInTx(context.Context, func(ctx context.Context) error) error
}

View File

@@ -103,3 +103,7 @@ func (enum *Email) UnmarshalText(text []byte) error {
return nil
}
func (enum Email) MarshalText() (text []byte, err error) {
return []byte(enum.StringValue()), nil
}

View File

@@ -73,3 +73,7 @@ func (enum *String) UnmarshalText(text []byte) error {
*enum = NewString(string(text))
return nil
}
func (enum String) MarshalText() (text []byte, err error) {
return []byte(enum.StringValue()), nil
}

View File

@@ -136,3 +136,7 @@ func (enum *UUID) UnmarshalText(text []byte) error {
*enum = uuid
return nil
}
func (enum UUID) MarshalText() (text []byte, err error) {
return []byte(enum.StringValue()), nil
}

View File

@@ -39,4 +39,7 @@ type Valuer interface {
// Implement encoding.TextUnmarshaler to allow the value to be unmarshalled from a string
encoding.TextUnmarshaler
// Implement encoding.TextUnmarshaler to allow the value to be marshalled unto a string
encoding.TextMarshaler
}