From 743ff125b58e2775546421611673829863bda8fb Mon Sep 17 00:00:00 2001 From: SimoneDutto Date: Mon, 26 Aug 2024 11:10:17 +0200 Subject: [PATCH] add patch identity entitlements --- internal/rebac_admin/identities.go | 58 +++++++- .../identities_integration_test.go | 128 +++++++++++++++++- 2 files changed, 180 insertions(+), 6 deletions(-) diff --git a/internal/rebac_admin/identities.go b/internal/rebac_admin/identities.go index 59f8b7b2e..2dac8ac0f 100644 --- a/internal/rebac_admin/identities.go +++ b/internal/rebac_admin/identities.go @@ -9,9 +9,10 @@ import ( "github.com/canonical/jimm/v3/internal/common/pagination" "github.com/canonical/jimm/v3/internal/jujuapi" "github.com/canonical/jimm/v3/internal/openfga" - "github.com/canonical/jimm/v3/internal/openfga/names" + ofganames "github.com/canonical/jimm/v3/internal/openfga/names" "github.com/canonical/jimm/v3/internal/rebac_admin/utils" apiparams "github.com/canonical/jimm/v3/pkg/api/params" + "github.com/juju/names/v5" v1 "github.com/canonical/rebac-admin-ui-handlers/v1" "github.com/canonical/rebac-admin-ui-handlers/v1/resources" @@ -112,7 +113,7 @@ func (s *identitiesService) GetIdentityGroups(ctx context.Context, identityId st filter := utils.CreateTokenPaginationFilter(params.Size, params.NextToken, params.NextPageToken) tuples, cNextToken, err := s.jimm.ListRelationshipTuples(ctx, user, apiparams.RelationshipTuple{ Object: objUser.ResourceTag().String(), - Relation: names.MemberRelation.String(), + Relation: ofganames.MemberRelation.String(), TargetObject: openfga.GroupType.String(), }, int32(filter.Limit()), filter.Token()) @@ -155,7 +156,7 @@ func (s *identitiesService) PatchIdentityGroups(ctx context.Context, identityId for _, p := range groupPatches { t := apiparams.RelationshipTuple{ Object: objUser.ResourceTag().String(), - Relation: names.MemberRelation.String(), + Relation: ofganames.MemberRelation.String(), TargetObject: p.Group, } if p.Op == "add" { @@ -214,5 +215,54 @@ func (s *identitiesService) GetIdentityEntitlements(ctx context.Context, identit // PatchIdentityEntitlements performs addition or removal of an Entitlement to/from an Identity. func (s *identitiesService) PatchIdentityEntitlements(ctx context.Context, identityId string, entitlementPatches []resources.IdentityEntitlementsPatchItem) (bool, error) { - return false, v1.NewNotImplementedError("get identity roles not implemented") + user, err := utils.GetUserFromContext(ctx) + if err != nil { + return false, err + } + objUser, err := s.jimm.FetchIdentity(ctx, identityId) + if err != nil { + return false, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) + } + var toAdd []apiparams.RelationshipTuple + var toRemove []apiparams.RelationshipTuple + var errList utils.MultiErr + toTargetTag := func(entitlementPatch resources.IdentityEntitlementsPatchItem) (names.Tag, error) { + return utils.ValidateDecomposedTag( + entitlementPatch.Entitlement.EntityType, + entitlementPatch.Entitlement.EntityId, + ) + } + for _, entitlementPatch := range entitlementPatches { + tag, err := toTargetTag(entitlementPatch) + if err != nil { + errList.AppendError(err) + continue + } + t := apiparams.RelationshipTuple{ + Object: objUser.Tag().String(), + Relation: entitlementPatch.Entitlement.Entitlement, + TargetObject: tag.String(), + } + if entitlementPatch.Op == resources.IdentityEntitlementsPatchItemOpAdd { + toAdd = append(toAdd, t) + } else { + toRemove = append(toRemove, t) + } + } + if err := errList.Error(); err != nil { + return false, err + } + if toAdd != nil { + err := s.jimm.AddRelation(ctx, user, toAdd) + if err != nil { + return false, err + } + } + if toRemove != nil { + err := s.jimm.RemoveRelation(ctx, user, toRemove) + if err != nil { + return false, err + } + } + return true, nil } diff --git a/internal/rebac_admin/identities_integration_test.go b/internal/rebac_admin/identities_integration_test.go index 965e14559..f09d93a2f 100644 --- a/internal/rebac_admin/identities_integration_test.go +++ b/internal/rebac_admin/identities_integration_test.go @@ -6,6 +6,7 @@ import ( rebac_handlers "github.com/canonical/rebac-admin-ui-handlers/v1" "github.com/canonical/rebac-admin-ui-handlers/v1/resources" + "github.com/juju/names/v5" gc "gopkg.in/check.v1" "github.com/canonical/jimm/v3/internal/jimmtest" @@ -14,7 +15,6 @@ import ( "github.com/canonical/jimm/v3/internal/rebac_admin" "github.com/canonical/jimm/v3/pkg/api/params" jimmnames "github.com/canonical/jimm/v3/pkg/names" - "github.com/juju/names/v5" ) type identitiesSuite struct { @@ -113,7 +113,7 @@ func (s *identitiesSuite) TestIdentityGetGroups(c *gc.C) { // TestIdentityEntitlements tests the listing of entitlements for a specific identityId. // Setup: add controllers, models to a user and add the user to a group. func (s *identitiesSuite) TestIdentityEntitlements(c *gc.C) { - // add user to 3 controllers and 3 models + // initialization ctx := context.Background() identitySvc := rebac_admin.NewidentitiesService(s.JIMM) groupTag := s.AddGroup(c, "test-group") @@ -143,6 +143,7 @@ func (s *identitiesSuite) TestIdentityEntitlements(c *gc.C) { err = s.JIMM.OpenFGAClient.AddRelation(ctx, tuples...) c.Assert(err, gc.IsNil) + // test ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser) emptyPageToken := "" req := resources.GetIdentitiesItemEntitlementsParams{NextPageToken: &emptyPageToken} @@ -185,3 +186,126 @@ func (s *identitiesSuite) TestIdentityEntitlements(c *gc.C) { c.Assert(controllerEntitlementCount, gc.Equals, 3) c.Assert(groupEntitlementCount, gc.Equals, 1) } + +// patchIdentitiesEntitlementTestEnv is used to create entries in JIMM's database. +// The rebacAdminSuite does not spin up a Juju controller so we cannot use +// regular JIMM methods to create resources. It is also necessary to have resources +// present in the database in order for ListRelationshipTuples to work correctly. +const patchIdentitiesEntitlementTestEnv = `clouds: +- name: test-cloud + type: test-provider + regions: + - name: test-cloud-region +cloud-credentials: +- owner: alice@canonical.com + name: cred-1 + cloud: test-cloud +controllers: +- name: controller-1 + uuid: 00000001-0000-0000-0000-000000000001 + cloud: test-cloud + region: test-cloud-region +models: +- name: model-1 + uuid: 00000002-0000-0000-0000-000000000001 + controller: controller-1 + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@canonical.com +- name: model-2 + uuid: 00000002-0000-0000-0000-000000000002 + controller: controller-1 + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@canonical.com +- name: model-3 + uuid: 00000003-0000-0000-0000-000000000003 + controller: controller-1 + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@canonical.com +- name: model-4 + uuid: 00000004-0000-0000-0000-000000000004 + controller: controller-1 + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@canonical.com +` + +// TestPatchIdentityEntitlements tests the patching of entitlements for a specific identityId, +// adding and removing relations after the setup. +// Setup: add user to a group, and add models to the user. +func (s *identitiesSuite) TestPatchIdentityEntitlements(c *gc.C) { + // initialization + ctx := context.Background() + identitySvc := rebac_admin.NewidentitiesService(s.JIMM) + tester := jimmtest.GocheckTester{C: c} + env := jimmtest.ParseEnvironment(tester, patchGroupEntitlementTestEnv) + env.PopulateDB(tester, s.JIMM.Database) + oldModels := []string{env.Models[0].UUID, env.Models[1].UUID} + newModels := []string{env.Models[2].UUID, env.Models[3].UUID} + user := names.NewUserTag("test-user@canonical.com") + s.AddUser(c, user.Id()) + tuple := openfga.Tuple{ + Object: ofganames.ConvertTag(user), + Relation: ofganames.AdministratorRelation, + } + + var tuples []openfga.Tuple + for i := range 2 { + t := tuple + t.Target = ofganames.ConvertTag(names.NewModelTag(oldModels[i])) + tuples = append(tuples, t) + } + err := s.JIMM.OpenFGAClient.AddRelation(ctx, tuples...) + c.Assert(err, gc.IsNil) + allowed, err := s.JIMM.OpenFGAClient.CheckRelation(ctx, tuples[0], false) + c.Assert(err, gc.IsNil) + c.Assert(allowed, gc.Equals, true) + // Above we have added granted the user with administrator permission to 2 models. + // Below, we will request those 2 relations to be removed and add 2 different relations. + + entitlementPatches := []resources.IdentityEntitlementsPatchItem{ + {Entitlement: resources.EntityEntitlement{ + Entitlement: ofganames.AdministratorRelation.String(), + EntityId: newModels[0], + EntityType: openfga.ModelType.String(), + }, Op: resources.IdentityEntitlementsPatchItemOpAdd}, + {Entitlement: resources.EntityEntitlement{ + Entitlement: ofganames.AdministratorRelation.String(), + EntityId: newModels[1], + EntityType: openfga.ModelType.String(), + }, Op: resources.IdentityEntitlementsPatchItemOpAdd}, + {Entitlement: resources.EntityEntitlement{ + Entitlement: ofganames.AdministratorRelation.String(), + EntityId: oldModels[0], + EntityType: openfga.ModelType.String(), + }, Op: resources.IdentityEntitlementsPatchItemOpRemove}, + {Entitlement: resources.EntityEntitlement{ + Entitlement: ofganames.AdministratorRelation.String(), + EntityId: oldModels[1], + EntityType: openfga.ModelType.String(), + }, Op: resources.IdentityEntitlementsPatchItemOpRemove}, + } + ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser) + res, err := identitySvc.PatchIdentityEntitlements(ctx, user.Id(), entitlementPatches) + c.Assert(err, gc.IsNil) + c.Assert(res, gc.Equals, true) + + for i := range 2 { + exists, err := s.JIMM.OpenFGAClient.CheckRelation(ctx, tuples[i], false) + c.Assert(err, gc.IsNil) + c.Assert(exists, gc.Equals, false) + } + for i := range 2 { + newTuple := tuples[0] + newTuple.Target = ofganames.ConvertTag(names.NewModelTag(newModels[i])) + allowed, err = s.JIMM.OpenFGAClient.CheckRelation(ctx, newTuple, false) + c.Assert(err, gc.IsNil) + c.Assert(allowed, gc.Equals, true) + } +}