feat(authz): openfga sql migration (#9580)

* feat(authz): openfga sql migration

* feat(authz): formatting and naming

* feat(authz): formatting and naming

* feat(authz): extract function for store and model id

* feat(authz): reorder the provider
This commit is contained in:
Vikrant Gupta
2025-11-14 00:43:02 +05:30
committed by GitHub
parent dbb6b333c8
commit 245179cbf7
24 changed files with 507 additions and 331 deletions

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

@@ -94,6 +94,130 @@ func (provider *provider) Stop(ctx context.Context) error {
return nil
}
func (provider *provider) Check(ctx context.Context, tupleReq *openfgav1.TupleKey) error {
storeID, modelID := provider.getStoreIDandModelID()
checkResponse, err := provider.openfgaServer.Check(
ctx,
&openfgav1.CheckRequest{
StoreId: storeID,
AuthorizationModelId: modelID,
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())
}
if !checkResponse.Allowed {
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subject %s cannot %s object %s", tupleReq.User, tupleReq.Relation, tupleReq.Object)
}
return nil
}
func (provider *provider) BatchCheck(ctx context.Context, tupleReq []*openfgav1.TupleKey) error {
storeID, modelID := provider.getStoreIDandModelID()
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: storeID,
AuthorizationModelId: 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, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeUser, claims.UserID, authtypes.Relation{})
if err != nil {
return err
}
tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID)
if err != nil {
return err
}
err = provider.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
storeID, modelID := provider.getStoreIDandModelID()
deletionTuplesWithoutCondition := make([]*openfgav1.TupleKeyWithoutCondition, len(deletions))
for idx, tuple := range deletions {
deletionTuplesWithoutCondition[idx] = &openfgav1.TupleKeyWithoutCondition{User: tuple.User, Object: tuple.Object, Relation: tuple.Relation}
}
_, err := provider.openfgaServer.Write(ctx, &openfgav1.WriteRequest{
StoreId: storeID,
AuthorizationModelId: modelID,
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,
}
}(),
})
return err
}
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, typeable authtypes.Typeable) ([]*authtypes.Object, error) {
storeID, modelID := provider.getStoreIDandModelID()
response, err := provider.openfgaServer.ListObjects(ctx, &openfgav1.ListObjectsRequest{
StoreId: storeID,
AuthorizationModelId: 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 authtypes.MustNewObjectsFromStringSlice(response.Objects), nil
}
func (provider *provider) getOrCreateStore(ctx context.Context, name string) (string, error) {
stores, err := provider.openfgaServer.ListStores(ctx, &openfgav1.ListStoresRequest{})
if err != nil {
@@ -176,112 +300,12 @@ func (provider *provider) isModelEqual(expected *openfgav1.AuthorizationModel, a
}
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: &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())
}
func (provider *provider) getStoreIDandModelID() (string, string) {
provider.mtx.RLock()
defer provider.mtx.RUnlock()
if !checkResponse.Allowed {
return errors.Newf(errors.TypeForbidden, authtypes.ErrCodeAuthZForbidden, "subject %s cannot %s object %s", tupleReq.User, tupleReq.Relation, tupleReq.Object)
}
storeID := provider.storeID
modelID := provider.modelID
return nil
}
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, orgID valuer.UUID, _ authtypes.Relation, translation authtypes.Relation, _ authtypes.Typeable, _ []authtypes.Selector) error {
subject, err := authtypes.NewSubject(authtypes.TypeUser, claims.UserID, authtypes.Relation{})
if err != nil {
return err
}
tuples, err := authtypes.TypeableOrganization.Tuples(subject, translation, []authtypes.Selector{authtypes.MustNewSelector(authtypes.TypeOrganization, orgID.StringValue())}, orgID)
if err != nil {
return err
}
err = provider.BatchCheck(ctx, tuples)
if err != nil {
return err
}
return nil
}
func (provider *provider) Write(ctx context.Context, additions []*openfgav1.TupleKey, deletions []*openfgav1.TupleKey) error {
deletionTuplesWithoutCondition := make([]*openfgav1.TupleKeyWithoutCondition, len(deletions))
for idx, tuple := range deletions {
deletionTuplesWithoutCondition[idx] = &openfgav1.TupleKeyWithoutCondition{User: tuple.User, Object: tuple.Object, Relation: tuple.Relation}
}
_, err := provider.openfgaServer.Write(ctx, &openfgav1.WriteRequest{
StoreId: provider.storeID,
AuthorizationModelId: provider.modelID,
Writes: &openfgav1.WriteRequestWrites{
TupleKeys: additions,
},
Deletes: &openfgav1.WriteRequestDeletes{
TupleKeys: deletionTuplesWithoutCondition,
},
})
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 authtypes.MustNewObjectsFromStringSlice(response.Objects), nil
return storeID, modelID
}

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,11 +28,6 @@ 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
}
req := new(roletypes.PostableRole)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
@@ -40,7 +35,7 @@ func (handler *handler) Create(rw http.ResponseWriter, r *http.Request) {
return
}
role, err := handler.module.Create(ctx, orgID, req.DisplayName, req.Description)
role, err := handler.module.Create(ctx, valuer.MustNewUUID(claims.OrgID), req.DisplayName, req.Description)
if err != nil {
render.Error(rw, err)
return
@@ -56,11 +51,6 @@ 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
}
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -73,7 +63,7 @@ func (handler *handler) Get(rw http.ResponseWriter, r *http.Request) {
return
}
role, err := handler.module.Get(ctx, orgID, roleID)
role, err := handler.module.Get(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
render.Error(rw, err)
return
@@ -89,11 +79,6 @@ 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
}
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -117,7 +102,7 @@ func (handler *handler) GetObjects(rw http.ResponseWriter, r *http.Request) {
return
}
objects, err := handler.module.GetObjects(ctx, orgID, roleID, relation)
objects, err := handler.module.GetObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation)
if err != nil {
render.Error(rw, err)
return
@@ -147,13 +132,8 @@ func (handler *handler) List(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
}
roles, err := handler.module.List(ctx, orgID)
roles, err := handler.module.List(ctx, valuer.MustNewUUID(claims.OrgID))
if err != nil {
render.Error(rw, err)
return
@@ -169,11 +149,6 @@ func (handler *handler) Patch(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
}
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -192,7 +167,7 @@ func (handler *handler) Patch(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.Patch(ctx, orgID, roleID, req.DisplayName, req.Description)
err = handler.module.Patch(ctx, valuer.MustNewUUID(claims.OrgID), roleID, req.DisplayName, req.Description)
if err != nil {
render.Error(rw, err)
return
@@ -208,11 +183,6 @@ 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
}
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -248,7 +218,7 @@ func (handler *handler) PatchObjects(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.PatchObjects(ctx, orgID, roleID, relation, patchableObjects.Additions, patchableObjects.Deletions)
err = handler.module.PatchObjects(ctx, valuer.MustNewUUID(claims.OrgID), roleID, relation, patchableObjects.Additions, patchableObjects.Deletions)
if err != nil {
render.Error(rw, err)
return
@@ -264,11 +234,6 @@ 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
}
id, ok := mux.Vars(r)["id"]
if !ok {
@@ -281,7 +246,7 @@ func (handler *handler) Delete(rw http.ResponseWriter, r *http.Request) {
return
}
err = handler.module.Delete(ctx, orgID, roleID)
err = handler.module.Delete(ctx, valuer.MustNewUUID(claims.OrgID), roleID)
if err != nil {
render.Error(rw, err)
return

View File

@@ -17,12 +17,12 @@ 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) {

View File

@@ -13,8 +13,8 @@ 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 {
@@ -38,7 +38,7 @@ func (store *store) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID)
BunDB().
NewSelect().
Model(role).
Where("orgID = ?", orgID).
Where("org_id = ?", orgID).
Where("id = ?", id).
Scan(ctx)
if err != nil {
@@ -55,7 +55,7 @@ func (store *store) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.S
BunDB().
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)

View File

@@ -138,6 +138,7 @@ func NewSQLMigrationProviderFactories(
sqlmigration.NewUpdateTTLSettingForCustomRetentionFactory(sqlstore, sqlschema),
sqlmigration.NewAddRoutePolicyFactory(sqlstore, sqlschema),
sqlmigration.NewAddAuthTokenFactory(sqlstore, sqlschema),
sqlmigration.NewAddAuthzFactory(sqlstore, sqlschema),
)
}

View File

@@ -0,0 +1,145 @@
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 addAuthz struct {
sqlstore sqlstore.SQLStore
sqlschema sqlschema.SQLSchema
}
func NewAddAuthzFactory(sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) factory.ProviderFactory[SQLMigration, Config] {
return factory.NewProviderFactory(factory.MustNewName("add_authz"), func(ctx context.Context, ps factory.ProviderSettings, c Config) (SQLMigration, error) {
return newAddAuthz(ctx, ps, c, sqlstore, sqlschema)
})
}
func newAddAuthz(_ context.Context, _ factory.ProviderSettings, _ Config, sqlstore sqlstore.SQLStore, sqlschema sqlschema.SQLSchema) (SQLMigration, error) {
return &addAuthz{
sqlstore: sqlstore,
sqlschema: sqlschema,
}, nil
}
func (migration *addAuthz) Register(migrations *migrate.Migrations) error {
if err := migrations.Register(migration.Up, migration.Down); err != nil {
return err
}
return nil
}
func (migration *addAuthz) 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: "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: "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},
{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...)
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", "ulid", "object_type"}},
})
sqls = append(sqls, tableSQLs...)
for _, sql := range sqls {
if _, err := tx.ExecContext(ctx, string(sql)); err != nil {
return err
}
}
err = tx.Commit()
if err != nil {
return err
}
return nil
}
func (migration *addAuthz) Down(context.Context, *bun.DB) error {
return nil
}

View File

@@ -9,6 +9,9 @@ import (
var (
nameRegex = regexp.MustCompile("^[a-z]{1,35}$")
_ json.Marshaler = new(Name)
_ json.Unmarshaler = new(Name)
)
type Name struct {
@@ -36,6 +39,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

@@ -23,8 +23,8 @@ 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},
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 {
@@ -72,6 +50,10 @@ func MustNewSelector(typed Type, input string) Selector {
return selector
}
func (selector *Selector) MarshalJSON() ([]byte, error) {
return json.Marshal(selector.val)
}
func (selector Selector) String() string {
return selector.val
}
@@ -83,8 +65,40 @@ 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())
}
return nil
case TypeRole:
if !typeRoleSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeRoleSelectorRegex.String())
}
return nil
case TypeOrganization:
if !typeOrganizationSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeOrganizationSelectorRegex.String())
}
return nil
case TypeMetaResource:
if !typeMetaResourceSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeMetaResourceSelectorRegex.String())
}
return nil
case TypeMetaResources:
if !typeMetaResourcesSelectorRegex.MatchString(selector) {
return errors.Newf(errors.TypeInvalidInput, ErrCodeAuthZInvalidSelector, "selector must conform to regex %s", typeMetaResourcesSelectorRegex.String())
}
return nil
}
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")}
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,48 @@
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
}
// example: metaresource:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboard
func (typeableMetaResource *typeableMetaResource) Prefix(orgID valuer.UUID) string {
return typeableMetaResource.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableMetaResource.Name().String()
}

View File

@@ -0,0 +1,48 @@
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
}
// example: metaresources:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/dashboards
func (typeableMetaResources *typeableMetaResources) Prefix(orgID valuer.UUID) string {
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

@@ -27,7 +27,7 @@ func (typeableRole *typeableRole) Name() Name {
return MustNewName("role")
}
// example: role:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/role
func (typeableRole *typeableRole) Prefix(orgID valuer.UUID) string {
// example: role:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/role
return typeableRole.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableRole.Name().String()
}

View File

@@ -27,7 +27,7 @@ func (typeableUser *typeableUser) Name() Name {
return MustNewName("user")
}
// example: user:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/user
func (typeableUser *typeableUser) Prefix(orgID valuer.UUID) string {
// example: user:organization/0199c47d-f61b-7833-bc5f-c0730f12f046/user
return typeableUser.Type().StringValue() + ":" + "organization" + "/" + orgID.StringValue() + "/" + typeableUser.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

@@ -22,7 +22,7 @@ var (
)
var (
TypeableResourcesRoles = authtypes.MustNewTypeableResources(authtypes.MustNewName("roles"))
TypeableResourcesRoles = authtypes.MustNewTypeableMetaResources(authtypes.MustNewName("roles"))
)
type StorableRole struct {

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
}