diff --git a/README.md b/README.md index 2bd35872..2564a867 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ These sections show how to use the SDK to perform API management functions. Befo 9. [Manage JWTs](#manage-jwts) 10. [Search Audit](#search-audit) 11. [Embedded Links](#embedded-links) +12. [Manage Authz](#manage-authz) If you wish to run any of our code samples and play with them, check out our [Code Examples](#code-examples) section. @@ -904,6 +905,181 @@ if err == nil { } ``` +### Manage Authz + +Descope supports full relation based access control (ReBAC) using a zanzibar like schema and operations. +A schema is comprized of namespaces (entities like documents, folders, orgs, etc.) and each namespace has relation definitions to define relations. +Each relation definition can be simple (either you have it or not) or complex (union of nodes). + +A simple example for a file system like schema would be: + +```yaml +# Example schema for the authz tests +name: Files +namespaces: + - name: org + relationDefinitions: + - name: parent + - name: member + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationLeft + relationDefinition: parent + relationDefinitionNamespace: org + targetRelationDefinition: member + targetRelationDefinitionNamespace: org + - name: folder + relationDefinitions: + - name: parent + - name: owner + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: editor + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: viewer + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: folder + targetRelationDefinition: viewer + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - name: doc + relationDefinitions: + - name: parent + - name: owner + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: owner + targetRelationDefinitionNamespace: folder + - name: editor + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: editor + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: owner + targetRelationDefinitionNamespace: doc + - name: viewer + complexDefinition: + nType: union + children: + - nType: child + expression: + neType: self + - nType: child + expression: + neType: relationRight + relationDefinition: parent + relationDefinitionNamespace: doc + targetRelationDefinition: viewer + targetRelationDefinitionNamespace: folder + - nType: child + expression: + neType: targetSet + targetRelationDefinition: editor + targetRelationDefinitionNamespace: doc +``` + +Descope SDK allows you to fully manage the schema and relations as well as perform simple (and not so simple) checks regarding the existence of relations. + +```go +// Load the existing schema +schema, err := descopeClient.Management.Authz().LoadSchema() +if err != nil { + // handle error +} + +// Save schema and make sure to remove all namespaces not listed +err := descopeClient.Management.Authz().SaveSchema(schema, true) + +// Create a relation between a resource and user +err := descopeClient.Management.Authz().CreateRelations([]*descope.AuthzRelation { + { + resource: "some-doc", + relationDefinition: "owner", + namespace: "doc", + target: "u1", + }, +}) + +// Check if target has the relevant relation +// The answer should be true because an owner is also a viewer +relations, err := descopeClient.Management.Authz().HasRelations([]*descope.AuthzRelationQuery{ + { + resource: "some-doc", + relationDefinition: "viewer", + namespace: "doc", + target: "u1", + } +}) +``` + ## Code Examples You can find various usage examples in the [examples folder](https://github.com/descope/go-sdk/blob/main/examples). diff --git a/descope/api/client.go b/descope/api/client.go index b7c6b78f..f1879b3f 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -133,6 +133,21 @@ var ( projectExport: "mgmt/project/export", projectImport: "mgmt/project/import", auditSearch: "mgmt/audit/search", + authzSchemaSave: "mgmt/authz/schema/save", + authzSchemaDelete: "mgmt/authz/schema/delete", + authzSchemaLoad: "mgmt/authz/schema/load", + authzNSSave: "mgmt/authz/ns/save", + authzNSDelete: "mgmt/authz/ns/delete", + authzRDSave: "mgmt/authz/rd/save", + authzRDDelete: "mgmt/authz/rd/delete", + authzRECreate: "mgmt/authz/re/create", + authzREDelete: "mgmt/authz/re/delete", + authzREDeleteResources: "mgmt/authz/re/deleteresources", + authzREHasRelations: "mgmt/authz/re/has", + authzREWho: "mgmt/authz/re/who", + authzREResource: "mgmt/authz/re/resource", + authzRETargets: "mgmt/authz/re/targets", + authzRETargetAll: "mgmt/authz/re/targetall", }, logout: "auth/logout", logoutAll: "auth/logoutall", @@ -268,6 +283,22 @@ type mgmtEndpoints struct { projectImport string auditSearch string + + authzSchemaSave string + authzSchemaDelete string + authzSchemaLoad string + authzNSSave string + authzNSDelete string + authzRDSave string + authzRDDelete string + authzRECreate string + authzREDelete string + authzREDeleteResources string + authzREHasRelations string + authzREWho string + authzREResource string + authzRETargets string + authzRETargetAll string } func (e *endpoints) SignInOTP() string { @@ -653,6 +684,66 @@ func (e *endpoints) ManagementAuditSearch() string { return path.Join(e.version, e.mgmt.auditSearch) } +func (e *endpoints) ManagementAuthzSchemaSave() string { + return path.Join(e.version, e.mgmt.authzSchemaSave) +} + +func (e *endpoints) ManagementAuthzSchemaDelete() string { + return path.Join(e.version, e.mgmt.authzSchemaDelete) +} + +func (e *endpoints) ManagementAuthzSchemaLoad() string { + return path.Join(e.version, e.mgmt.authzSchemaLoad) +} + +func (e *endpoints) ManagementAuthzNSSave() string { + return path.Join(e.version, e.mgmt.authzNSSave) +} + +func (e *endpoints) ManagementAuthzNSDelete() string { + return path.Join(e.version, e.mgmt.authzNSDelete) +} + +func (e *endpoints) ManagementAuthzRDSave() string { + return path.Join(e.version, e.mgmt.authzRDSave) +} + +func (e *endpoints) ManagementAuthzRDDelete() string { + return path.Join(e.version, e.mgmt.authzRDDelete) +} + +func (e *endpoints) ManagementAuthzRECreate() string { + return path.Join(e.version, e.mgmt.authzRECreate) +} + +func (e *endpoints) ManagementAuthzREDelete() string { + return path.Join(e.version, e.mgmt.authzREDelete) +} + +func (e *endpoints) ManagementAuthzREDeleteResources() string { + return path.Join(e.version, e.mgmt.authzREDeleteResources) +} + +func (e *endpoints) ManagementAuthzREHasRelations() string { + return path.Join(e.version, e.mgmt.authzREHasRelations) +} + +func (e *endpoints) ManagementAuthzREWho() string { + return path.Join(e.version, e.mgmt.authzREWho) +} + +func (e *endpoints) ManagementAuthzREResource() string { + return path.Join(e.version, e.mgmt.authzREResource) +} + +func (e *endpoints) ManagementAuthzRETargets() string { + return path.Join(e.version, e.mgmt.authzRETargets) +} + +func (e *endpoints) ManagementAuthzRETargetAll() string { + return path.Join(e.version, e.mgmt.authzRETargetAll) +} + type sdkInfo struct { name string version string diff --git a/descope/internal/mgmt/mgmt.go b/descope/internal/mgmt/mgmt.go index 236de0c1..2eeacc13 100644 --- a/descope/internal/mgmt/mgmt.go +++ b/descope/internal/mgmt/mgmt.go @@ -31,6 +31,7 @@ type managementService struct { flow sdk.Flow project sdk.Project audit sdk.Audit + authz sdk.Authz } func NewManagement(conf ManagementParams, c *api.Client) *managementService { @@ -47,6 +48,7 @@ func NewManagement(conf ManagementParams, c *api.Client) *managementService { service.flow = &flow{managementBase: base} service.project = &project{managementBase: base} service.audit = &audit{managementBase: base} + service.authz = &authz{managementBase: base} return service } @@ -105,6 +107,11 @@ func (mgmt *managementService) Audit() sdk.Audit { return mgmt.audit } +func (mgmt *managementService) Authz() sdk.Authz { + mgmt.ensureManagementKey() + return mgmt.authz +} + func (mgmt *managementService) ensureManagementKey() { if mgmt.conf.ManagementKey == "" { logger.LogInfo("Management key is missing, make sure to add it in the Config struct or the environment variable \"%s\"", descope.EnvironmentVariableManagementKey) // notest diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index b4cde5e8..de7d40d7 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -443,6 +443,63 @@ type Audit interface { Search(*descope.AuditSearchOptions) ([]*descope.AuditRecord, error) } +// Provides authorization ReBAC capabilities +type Authz interface { + // SaveSchema creating or updating it. + // In case of update, will update only given namespaces and will not delete namespaces unless upgrade flag is true. + // Schema name can be used for projects to track versioning. + SaveSchema(schema *descope.AuthzSchema, upgrade bool) error + + // DeleteSchema for the project which will also delete all relations. + DeleteSchema() error + + // LoadSchema for the project. + LoadSchema() (*descope.AuthzSchema, error) + + // SaveNamespace creating or updating the given namespace + // Will not delete relation definitions not mentioned in the namespace. + // oldName is used if we are changing the namespace name + // schemaName is optional and used to track the current schema version. + SaveNamespace(namespace *descope.AuthzNamespace, oldName, schemaName string) error + + // DeleteNamespace will also delete the relevant relations. + // schemaName is optional and used to track the current schema version. + DeleteNamespace(name, schemaName string) error + + // SaveRelationDefinition creating or updating the given relation definition. + // Provide oldName if we are changing the relation definition name, what was the old name we are updating. + // schemaName optional and used to track the current schema version. + SaveRelationDefinition(relationDefinition *descope.AuthzRelationDefinition, namespace, oldName, schemaName string) error + + // DeleteRelationDefinition will also delete the relevant relations. + // schemaName is optional and used to track the current schema version. + DeleteRelationDefinition(name, namespace, schemaName string) error + + // CreateRelations based on the existing schema + CreateRelations(relations []*descope.AuthzRelation) error + + // DeleteRelations based on the existing schema + DeleteRelations(relations []*descope.AuthzRelation) error + + // DeleteRelationsForResources will delete all relations to the given resources + DeleteRelationsForResources(resources []string) error + + // HasRelations check queries given relations to see if they exist returning true if they do + HasRelations(relationQueries []*descope.AuthzRelationQuery) ([]*descope.AuthzRelationQuery, error) + + // WhoCanAccess the given resource returns the list of targets with the given relation definition + WhoCanAccess(resource, relationDefinition, namespace string) ([]string, error) + + // ResourceRelations returns the list of all defined relations (not recursive) on the given resource. + ResourceRelations(resource string) ([]*descope.AuthzRelation, error) + + // TargetRelations returns the list of all defined relations (not recursive) for the given targets. + TargetsRelations(targets []string) ([]*descope.AuthzRelation, error) + + // WhatCanTargetAccess returns the list of all relations for the given target including derived relations from the schema tree. + WhatCanTargetAccess(target string) ([]*descope.AuthzRelation, error) +} + // Provides various APIs for managing a Descope project programmatically. A management key must // be provided in the DecopeClient configuration or by setting the DESCOPE_MANAGEMENT_KEY // environment variable. Management keys can be generated in the Descope console. @@ -479,4 +536,7 @@ type Management interface { // Provide functions for managing projects Project() Project + + // Provides functions for ReBAC authz management + Authz() Authz } diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index fd30f2b3..3d6d589b 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -17,6 +17,7 @@ type MockManagement struct { *MockFlow *MockProject *MockAudit + *MockAuthz } func (m *MockManagement) JWT() sdk.JWT { @@ -63,6 +64,10 @@ func (m *MockManagement) Audit() sdk.Audit { return m.MockAudit } +func (m *MockManagement) Authz() sdk.Authz { + return m.MockAuthz +} + // Mock JWT type MockJWT struct { @@ -842,3 +847,153 @@ func (m *MockAudit) Search(options *descope.AuditSearchOptions) ([]*descope.Audi } return m.SearchResponse, m.SearchError } + +type MockAuthz struct { + SaveSchemaAssert func(schema *descope.AuthzSchema, upgrade bool) + SaveSchemaError error + + DeleteSchemaError error + + LoadSchemaResponse *descope.AuthzSchema + LoadSchemaError error + + SaveNamespaceAssert func(namespace *descope.AuthzNamespace, oldName, schemaName string) + SaveNamespaceError error + + DeleteNamespaceAssert func(name, schemaName string) + DeleteNamespaceError error + + SaveRelationDefinitionAssert func(relationDefinition *descope.AuthzRelationDefinition, namespace, oldName, schemaName string) + SaveRelationDefinitionError error + + DeleteRelationDefinitionAssert func(name, namespace, schemaName string) + DeleteRelationDefinitionError error + + CreateRelationsAssert func(relations []*descope.AuthzRelation) + CreateRelationsError error + + DeleteRelationsAssert func(relations []*descope.AuthzRelation) + DeleteRelationsError error + + DeleteRelationsForResourcesAssert func(resources []string) + DeleteRelationsForResourcesError error + + HasRelationsAssert func(relationQueries []*descope.AuthzRelationQuery) + HasRelationsResponse []*descope.AuthzRelationQuery + HasRelationsError error + + WhoCanAccessAssert func(resource, relationDefinition, namespace string) + WhoCanAccessResponse []string + WhoCanAccessError error + + ResourceRelationsAssert func(resource string) + ResourceRelationsResponse []*descope.AuthzRelation + ResourceRelationsError error + + TargetsRelationsAssert func(targets []string) + TargetsRelationsResponse []*descope.AuthzRelation + TargetsRelationsError error + + WhatCanTargetAccessAssert func(target string) + WhatCanTargetAccessResponse []*descope.AuthzRelation + WhatCanTargetAccessError error +} + +func (m *MockAuthz) SaveSchema(schema *descope.AuthzSchema, upgrade bool) error { + if m.SaveSchemaAssert != nil { + m.SaveSchemaAssert(schema, upgrade) + } + return m.SaveSchemaError +} + +func (m *MockAuthz) DeleteSchema() error { + return m.DeleteSchemaError +} + +func (m *MockAuthz) LoadSchema() (*descope.AuthzSchema, error) { + return m.LoadSchemaResponse, m.LoadSchemaError +} + +func (m *MockAuthz) SaveNamespace(namespace *descope.AuthzNamespace, oldName, schemaName string) error { + if m.SaveNamespaceAssert != nil { + m.SaveNamespaceAssert(namespace, oldName, schemaName) + } + return m.SaveNamespaceError +} + +func (m *MockAuthz) DeleteNamespace(name, schemaName string) error { + if m.DeleteNamespaceAssert != nil { + m.DeleteNamespaceAssert(name, schemaName) + } + return m.DeleteNamespaceError +} + +func (m *MockAuthz) SaveRelationDefinition(relationDefinition *descope.AuthzRelationDefinition, namespace, oldName, schemaName string) error { + if m.SaveRelationDefinitionAssert != nil { + m.SaveRelationDefinitionAssert(relationDefinition, namespace, oldName, schemaName) + } + return m.SaveRelationDefinitionError +} + +func (m *MockAuthz) DeleteRelationDefinition(name, namespace, schemaName string) error { + if m.DeleteRelationDefinitionAssert != nil { + m.DeleteRelationDefinitionAssert(name, namespace, schemaName) + } + return m.DeleteRelationDefinitionError +} + +func (m *MockAuthz) CreateRelations(relations []*descope.AuthzRelation) error { + if m.CreateRelationsAssert != nil { + m.CreateRelationsAssert(relations) + } + return m.CreateRelationsError +} + +func (m *MockAuthz) DeleteRelations(relations []*descope.AuthzRelation) error { + if m.DeleteRelationsAssert != nil { + m.DeleteRelationsAssert(relations) + } + return m.DeleteRelationsError +} + +func (m *MockAuthz) DeleteRelationsForResources(resources []string) error { + if m.DeleteRelationsForResourcesAssert != nil { + m.DeleteRelationsForResourcesAssert(resources) + } + return m.DeleteRelationsForResourcesError +} + +func (m *MockAuthz) HasRelations(relationQueries []*descope.AuthzRelationQuery) ([]*descope.AuthzRelationQuery, error) { + if m.HasRelationsAssert != nil { + m.HasRelationsAssert(relationQueries) + } + return m.HasRelationsResponse, m.HasRelationsError +} + +func (m *MockAuthz) WhoCanAccess(resource, relationDefinition, namespace string) ([]string, error) { + if m.WhoCanAccessAssert != nil { + m.WhoCanAccessAssert(resource, relationDefinition, namespace) + } + return m.WhoCanAccessResponse, m.WhoCanAccessError +} + +func (m *MockAuthz) ResourceRelations(resource string) ([]*descope.AuthzRelation, error) { + if m.ResourceRelationsAssert != nil { + m.ResourceRelationsAssert(resource) + } + return m.ResourceRelationsResponse, m.ResourceRelationsError +} + +func (m *MockAuthz) TargetsRelations(targets []string) ([]*descope.AuthzRelation, error) { + if m.TargetsRelationsAssert != nil { + m.TargetsRelationsAssert(targets) + } + return m.TargetsRelationsResponse, m.TargetsRelationsError +} + +func (m *MockAuthz) WhatCanTargetAccess(target string) ([]*descope.AuthzRelation, error) { + if m.WhatCanTargetAccessAssert != nil { + m.WhatCanTargetAccessAssert(target) + } + return m.WhatCanTargetAccessResponse, m.WhatCanTargetAccessError +} diff --git a/examples/managementcli/main.go b/examples/managementcli/main.go index a01baf05..c5de961a 100644 --- a/examples/managementcli/main.go +++ b/examples/managementcli/main.go @@ -425,6 +425,33 @@ func auditFullTextSearch(args []string) error { return err } +func authzLoadSchema(args []string) error { + res, err := descopeClient.Management.Authz().LoadSchema() + if err == nil { + var b []byte + b, err = json.MarshalIndent(res, "", " ") + fmt.Println(string(b)) + } + return err +} + +func authzHasRelations(args []string) error { + res, err := descopeClient.Management.Authz().HasRelations([]*descope.AuthzRelationQuery{ + { + Resource: args[0], + RelationDefinition: args[1], + Namespace: args[2], + Target: args[3], + }, + }) + if err == nil { + var b []byte + b, err = json.MarshalIndent(res, "", " ") + fmt.Println(string(b)) + } + return err +} + // Command line setup var cli = &cobra.Command{ @@ -620,6 +647,13 @@ func main() { cmd.Args = cobra.ExactArgs(2) }) + addCommand(authzLoadSchema, "authz-load-schema", "Load and display the current AuthZ ReBAC schema", func(cmd *cobra.Command) { + }) + + addCommand(authzHasRelations, "authz-has-relation ", "Check if the given relation exists", func(cmd *cobra.Command) { + cmd.Args = cobra.ExactArgs(4) + }) + err := cli.Execute() if err != nil { os.Exit(1)