diff --git a/descope/api/client.go b/descope/api/client.go index 6d771f31..b3dc7038 100644 --- a/descope/api/client.go +++ b/descope/api/client.go @@ -187,6 +187,7 @@ var ( authzREResource: "mgmt/authz/re/resource", authzRETargets: "mgmt/authz/re/targets", authzRETargetAll: "mgmt/authz/re/targetall", + authzRETargetWithRelation: "mgmt/authz/re/targetwithrelation", authzGetModified: "mgmt/authz/getmodified", }, logout: "auth/logout", @@ -369,22 +370,23 @@ type mgmtEndpoints struct { 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 - authzGetModified 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 + authzRETargetWithRelation string + authzGetModified string } func (e *endpoints) SignInOTP() string { @@ -1017,6 +1019,10 @@ func (e *endpoints) ManagementAuthzRETargetAll() string { return path.Join(e.version, e.mgmt.authzRETargetAll) } +func (e *endpoints) ManagementAuthzRETargetWithRelation() string { + return path.Join(e.version, e.mgmt.authzRETargetWithRelation) +} + func (e *endpoints) ManagementAuthzGetModified() string { return path.Join(e.version, e.mgmt.authzGetModified) } diff --git a/descope/internal/mgmt/authz.go b/descope/internal/mgmt/authz.go index 0294a07c..f240c6a0 100644 --- a/descope/internal/mgmt/authz.go +++ b/descope/internal/mgmt/authz.go @@ -214,6 +214,10 @@ type relationsResponse struct { Relations []*descope.AuthzRelation `json:"relations"` } +type resourcesResponse struct { + Resources []string `json:"resources"` +} + func (a *authz) ResourceRelations(ctx context.Context, resource string) ([]*descope.AuthzRelation, error) { if resource == "" { return nil, utils.NewInvalidArgumentError("resource") @@ -277,6 +281,45 @@ func (a *authz) WhatCanTargetAccess(ctx context.Context, target string) ([]*desc return response.Relations, nil } +func (a *authz) WhatCanTargetAccessWithRelation(ctx context.Context, target, relationDefinition, namespace string) ([]*descope.AuthzRelation, error) { + if target == "" { + return nil, utils.NewInvalidArgumentError("target") + } + if relationDefinition == "" { + return nil, utils.NewInvalidArgumentError("relationDefinition") + } + if namespace == "" { + return nil, utils.NewInvalidArgumentError("namespace") + } + body := map[string]any{ + "target": target, + "relationDefinition": relationDefinition, + "namespace": namespace, + } + res, err := a.client.DoPostRequest(ctx, api.Routes.ManagementAuthzRETargetWithRelation(), body, nil, a.conf.ManagementKey) + if err != nil { + // notest + return nil, err + } + var response *resourcesResponse + err = utils.Unmarshal([]byte(res.BodyStr), &response) + if err != nil { + // notest + return nil, err + } + + var resp []*descope.AuthzRelation + for _, resource := range response.Resources { + resp = append(resp, &descope.AuthzRelation{ + Resource: resource, + Target: target, + RelationDefinition: relationDefinition, + Namespace: namespace, + }) + } + return resp, nil +} + func (a *authz) GetModified(ctx context.Context, since time.Time) (*descope.AuthzModified, error) { body := map[string]any{} if !since.IsZero() { diff --git a/descope/internal/mgmt/authz_test.go b/descope/internal/mgmt/authz_test.go index 9b2f0705..7468cacc 100644 --- a/descope/internal/mgmt/authz_test.go +++ b/descope/internal/mgmt/authz_test.go @@ -358,6 +358,46 @@ func TestWhatCanTargetAccessMissingArgument(t *testing.T) { require.ErrorContains(t, err, utils.NewInvalidArgumentError("target").Message) } +func TestWhatCanTargetAccessWithRelationSuccess(t *testing.T) { + response := []*descope.AuthzRelation{ + { + Resource: "r1", + RelationDefinition: "rd", + Namespace: "n", + Target: "u1", + }, + { + Resource: "r2", + RelationDefinition: "rd", + Namespace: "n", + Target: "u1", + }, + } + mgmt := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) { + require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key") + req := map[string]any{} + require.NoError(t, helpers.ReadBody(r, &req)) + require.Equal(t, "u1", req["target"]) + require.Equal(t, "rd", req["relationDefinition"]) + require.Equal(t, "n", req["namespace"]) + }, map[string]any{"resources": []string{"r1", "r2"}})) + res, err := mgmt.Authz().WhatCanTargetAccessWithRelation(context.Background(), "u1", "rd", "n") + require.NoError(t, err) + assert.EqualValues(t, response, res) +} + +func TestWhatCanTargetAccessWithRelationMissingArgument(t *testing.T) { + mgmt := newTestMgmt(nil, helpers.DoOk(nil)) + _, err := mgmt.Authz().WhatCanTargetAccessWithRelation(context.Background(), "", "", "") + require.ErrorContains(t, err, utils.NewInvalidArgumentError("target").Message) + + _, err = mgmt.Authz().WhatCanTargetAccessWithRelation(context.Background(), "tar", "", "") + require.ErrorContains(t, err, utils.NewInvalidArgumentError("relationDefinition").Message) + + _, err = mgmt.Authz().WhatCanTargetAccessWithRelation(context.Background(), "tar", "def", "") + require.ErrorContains(t, err, utils.NewInvalidArgumentError("namespace").Message) +} + func TestGetModifiedWrongArgument(t *testing.T) { mgmt := newTestMgmt(nil, helpers.DoOk(nil)) _, err := mgmt.Authz().GetModified(context.Background(), time.Now().Add(10*time.Second)) diff --git a/descope/sdk/mgmt.go b/descope/sdk/mgmt.go index ff0c06c6..5c320701 100644 --- a/descope/sdk/mgmt.go +++ b/descope/sdk/mgmt.go @@ -757,6 +757,9 @@ type Authz interface { // WhatCanTargetAccess returns the list of all relations for the given target including derived relations from the schema tree. WhatCanTargetAccess(ctx context.Context, target string) ([]*descope.AuthzRelation, error) + // WhatCanTargetAccessWithRelation returns the list of all resources that the target has the given relation to including all derived relations + WhatCanTargetAccessWithRelation(ctx context.Context, target, relationDefinition, namespace string) ([]*descope.AuthzRelation, error) + // GetModified list of targets and resources changed since the given date // Should be used to invalidate local caches GetModified(ctx context.Context, since time.Time) (*descope.AuthzModified, error) diff --git a/descope/tests/mocks/mgmt/managementmock.go b/descope/tests/mocks/mgmt/managementmock.go index 6ee25b2a..0b852bd8 100644 --- a/descope/tests/mocks/mgmt/managementmock.go +++ b/descope/tests/mocks/mgmt/managementmock.go @@ -1280,6 +1280,10 @@ type MockAuthz struct { WhatCanTargetAccessResponse []*descope.AuthzRelation WhatCanTargetAccessError error + WhatCanTargetAccessWithRelationAssert func(target, relationDefinition, namespace string) + WhatCanTargetAccessWithRelationResponse []*descope.AuthzRelation + WhatCanTargetAccessWithRelationError error + GetModifiedAssert func(since time.Time) GetModifiedResponse *descope.AuthzModified GetModifiedError error @@ -1384,6 +1388,13 @@ func (m *MockAuthz) WhatCanTargetAccess(_ context.Context, target string) ([]*de return m.WhatCanTargetAccessResponse, m.WhatCanTargetAccessError } +func (m *MockAuthz) WhatCanTargetAccessWithRelation(_ context.Context, target, relationDefinition, namespace string) ([]*descope.AuthzRelation, error) { + if m.WhatCanTargetAccessWithRelationAssert != nil { + m.WhatCanTargetAccessWithRelationAssert(target, relationDefinition, namespace) + } + return m.WhatCanTargetAccessWithRelationResponse, m.WhatCanTargetAccessWithRelationError +} + func (m *MockAuthz) GetModified(_ context.Context, since time.Time) (*descope.AuthzModified, error) { if m.GetModifiedAssert != nil { m.GetModifiedAssert(since)