Skip to content

Commit

Permalink
feat: add rebac-admin handlers for roles (#1469)
Browse files Browse the repository at this point in the history
Handlers are implemented identically as those for groups.
  • Loading branch information
kian99 authored Nov 28, 2024
1 parent 436ba45 commit 6031feb
Show file tree
Hide file tree
Showing 10 changed files with 1,035 additions and 12 deletions.
1 change: 1 addition & 0 deletions internal/jimmhttp/rebac_admin/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func SetupBackend(ctx context.Context, jimm jujuapi.JIMM) (*rebac_handlers.ReBAC
Identities: newidentitiesService(jimm),
Resources: newResourcesService(jimm),
Capabilities: newCapabilitiesService(),
Roles: newRoleService(jimm),
})
if err != nil {
zapctx.Error(ctx, "failed to create rebac admin backend", zap.Error(err))
Expand Down
2 changes: 2 additions & 0 deletions internal/jimmhttp/rebac_admin/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package rebac_admin

var (
NewGroupService = newGroupService
NewRoleService = newRoleService
NewidentitiesService = newidentitiesService
NewResourcesService = newResourcesService
NewEntitlementService = newEntitlementService
Expand All @@ -11,3 +12,4 @@ var (
)

type GroupsService = groupsService
type RolesService = rolesService
107 changes: 97 additions & 10 deletions internal/jimmhttp/rebac_admin/identities.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,19 +93,105 @@ func (s *identitiesService) UpdateIdentity(ctx context.Context, identity *resour
return nil, v1.NewNotImplementedError("update identity not implemented")
}

// // DeleteIdentity deletes an Identity.
// DeleteIdentity deletes an Identity.
func (s *identitiesService) DeleteIdentity(ctx context.Context, identityId string) (bool, error) {
return false, v1.NewNotImplementedError("delete identity not implemented")
}

// // GetIdentityRoles returns a page of Roles for identity `identityId`.
// GetIdentityRoles returns a page of identities in a Role identified by `roleId`.
func (s *identitiesService) GetIdentityRoles(ctx context.Context, identityId string, params *resources.GetIdentitiesItemRolesParams) (*resources.PaginatedResponse[resources.Role], error) {
return nil, v1.NewNotImplementedError("get identity roles not implemented")
user, err := utils.GetUserFromContext(ctx)
if err != nil {
return nil, err
}
objUser, err := s.jimm.FetchIdentity(ctx, identityId)
if err != nil {
return nil, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId))
}
filter := utils.CreateTokenPaginationFilter(params.Size, params.NextToken, params.NextPageToken)
tuples, cNextToken, err := s.jimm.ListRelationshipTuples(ctx, user, apiparams.RelationshipTuple{
Object: objUser.ResourceTag().String(),
Relation: ofganames.AssigneeRelation.String(),
TargetObject: openfga.RoleType.String(),
}, int32(filter.Limit()), filter.Token()) // #nosec G115 accept integer conversion
if err != nil {
return nil, err
}

roles := make([]resources.Role, 0, len(tuples))
for _, t := range tuples {
dbRole, err := s.jimm.GetRoleManager().GetRoleByUUID(ctx, user, t.Target.ID)
if err != nil {
// Handle the case where the role was removed from the DB but a lingering OpenFGA tuple still exists.
// Don't return an error as that would prevent a user from viewing their groups, instead drop the role from the result.
if errors.ErrorCode(err) == errors.CodeNotFound {
continue
}
return nil, err
}
roles = append(roles, resources.Role{
Id: &t.Target.ID,
Name: dbRole.Name,
})
}

originalToken := filter.Token()
res := resources.PaginatedResponse[resources.Role]{
Data: roles,
Meta: resources.ResponseMeta{
Size: len(roles),
PageToken: &originalToken,
},
}
if cNextToken != "" {
res.Next.PageToken = &cNextToken
}
return &res, nil
}

// // PatchIdentityRoles performs addition or removal of a Role to/from an Identity.
// PatchRoleIdentities performs addition or removal of identities to/from a Role identified by `roleId`.
func (s *identitiesService) PatchIdentityRoles(ctx context.Context, identityId string, rolePatches []resources.IdentityRolesPatchItem) (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))
}
additions := make([]apiparams.RelationshipTuple, 0)
deletions := make([]apiparams.RelationshipTuple, 0)
for _, p := range rolePatches {
if !jimmnames.IsValidRoleId(p.Role) {
return false, v1.NewValidationError(fmt.Sprintf("ID %s is not a valid role ID", p.Role))
}
t := apiparams.RelationshipTuple{
Object: objUser.ResourceTag().String(),
Relation: ofganames.AssigneeRelation.String(),
TargetObject: jimmnames.NewRoleTag(p.Role).String(),
}
if p.Op == resources.IdentityRolesPatchItemOpAdd {
additions = append(additions, t)
} else if p.Op == "remove" {
deletions = append(deletions, t)
}
}
if len(additions) > 0 {
err = s.jimm.AddRelation(ctx, user, additions)
if err != nil {
zapctx.Error(context.Background(), "cannot add relations", zap.Error(err))
return false, v1.NewUnknownError(err.Error())
}
}
if len(deletions) > 0 {
err = s.jimm.RemoveRelation(ctx, user, deletions)
if err != nil {
zapctx.Error(context.Background(), "cannot remove relations", zap.Error(err))
return false, v1.NewUnknownError(err.Error())
}
}
return true, nil
}

// GetIdentityGroups returns a page of Groups for identity `identityId`.
Expand Down Expand Up @@ -146,16 +232,17 @@ func (s *identitiesService) GetIdentityGroups(ctx context.Context, identityId st
}

originalToken := filter.Token()
return &resources.PaginatedResponse[resources.Group]{
res := resources.PaginatedResponse[resources.Group]{
Data: groups,
Meta: resources.ResponseMeta{
Size: len(groups),
PageToken: &originalToken,
},
Next: resources.Next{
PageToken: &cNextToken,
},
}, nil
}
if cNextToken != "" {
res.Next.PageToken = &cNextToken
}
return &res, nil
}

// PatchIdentityGroups performs addition or removal of a Group to/from an Identity.
Expand Down
98 changes: 96 additions & 2 deletions internal/jimmhttp/rebac_admin/identities_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,21 +127,24 @@ func (s *identitiesSuite) TestIdentityGetGroups(c *gc.C) {
// test list identity's groups with token pagination
size := 3
token := ""
totalGroups := 0
for i := 0; ; i += size {
groups, err := identitySvc.GetIdentityGroups(ctx, username, &resources.GetIdentitiesItemGroupsParams{
Size: &size,
NextToken: &token,
})
c.Assert(err, gc.IsNil)
token = *groups.Next.PageToken
for j := 0; j < len(groups.Data); j++ {
totalGroups++
c.Assert(groups.Data[j].Name, gc.Matches, `group-test\d+`)
c.Assert(groupTags[j].Id(), gc.Matches, `\w*-\w*-\w*-\w*-\w*`)
}
if *groups.Next.PageToken == "" {
if groups.Next.PageToken == nil || *groups.Next.PageToken == "" {
break
}
token = *groups.Next.PageToken
}
c.Assert(totalGroups, gc.Equals, groupsSize)
}

// TestGetIdentityGroupsWithDeletedDbGroup tests the behaviour
Expand Down Expand Up @@ -183,6 +186,97 @@ func (s *identitiesSuite) TestGetIdentityGroupsWithDeletedDbGroup(c *gc.C) {
c.Assert(groups.Data, gc.HasLen, 1)
}

func (s *identitiesSuite) TestIdentityPatchRoles(c *gc.C) {
// initialization
ctx := context.Background()
ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
identitySvc := rebac_admin.NewidentitiesService(s.JIMM)
roleName := "role-test1"
username := s.AdminUser.Name
role := s.AddRole(c, roleName)

// test add identity role
changed, err := identitySvc.PatchIdentityRoles(ctx, username, []resources.IdentityRolesPatchItem{{
Role: role.UUID,
Op: resources.IdentityRolesPatchItemOpAdd,
}})
c.Assert(err, gc.IsNil)
c.Assert(changed, gc.Equals, true)

// test user added to roles
objUser, err := s.JIMM.FetchIdentity(ctx, username)
c.Assert(err, gc.IsNil)
tuples, _, err := s.JIMM.ListRelationshipTuples(ctx, s.AdminUser, params.RelationshipTuple{
Object: objUser.ResourceTag().String(),
Relation: ofganames.AssigneeRelation.String(),
TargetObject: role.ResourceTag().String(),
}, 10, "")
c.Assert(err, gc.IsNil)
c.Assert(len(tuples), gc.Equals, 1)
c.Assert(role.UUID, gc.Equals, tuples[0].Target.ID)

// test user remove from role
changed, err = identitySvc.PatchIdentityRoles(ctx, username, []resources.IdentityRolesPatchItem{{
Role: role.UUID,
Op: resources.IdentityRolesPatchItemOpRemove,
}})
c.Assert(err, gc.IsNil)
c.Assert(changed, gc.Equals, true)
tuples, _, err = s.JIMM.ListRelationshipTuples(ctx, s.AdminUser, params.RelationshipTuple{
Object: objUser.ResourceTag().String(),
Relation: ofganames.AssigneeRelation.String(),
TargetObject: role.ResourceTag().String(),
}, 10, "")
c.Assert(err, gc.IsNil)
c.Assert(len(tuples), gc.Equals, 0)
}

func (s *identitiesSuite) TestIdentityGetRoles(c *gc.C) {
// initialization
ctx := context.Background()
ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
identitySvc := rebac_admin.NewidentitiesService(s.JIMM)
username := s.AdminUser.Name
rolesSize := 10
rolesToAdd := make([]resources.IdentityRolesPatchItem, rolesSize)
roleTags := make([]jimmnames.RoleTag, rolesSize)
for i := range rolesSize {
roleName := fmt.Sprintf("role-test%d", i)
role := s.AddRole(c, roleName)
roleTags[i] = role.ResourceTag()
rolesToAdd[i] = resources.IdentityRolesPatchItem{
Role: role.UUID,
Op: resources.IdentityRolesPatchItemOpAdd,
}

}
changed, err := identitySvc.PatchIdentityRoles(ctx, username, rolesToAdd)
c.Assert(err, gc.IsNil)
c.Assert(changed, gc.Equals, true)

// test list identity's roles with token pagination
size := 3
token := ""
totalRoles := 0
for i := 0; ; i += size {
roles, err := identitySvc.GetIdentityRoles(ctx, username, &resources.GetIdentitiesItemRolesParams{
Size: &size,
NextToken: &token,
})
c.Assert(err, gc.IsNil)
for j := 0; j < len(roles.Data); j++ {
totalRoles++
c.Assert(roles.Data[j].Name, gc.Matches, `role-test\d+`)
c.Assert(roleTags[j].Id(), gc.Matches, `\w*-\w*-\w*-\w*-\w*`)
}
if roles.Next.PageToken == nil || *roles.Next.PageToken == "" {
break
}
token = *roles.Next.PageToken
}
c.Assert(totalRoles, gc.Equals, rolesSize)
}

// 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) {
Expand Down
102 changes: 102 additions & 0 deletions internal/jimmhttp/rebac_admin/identities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import (
"github.com/canonical/jimm/v3/internal/common/utils"
"github.com/canonical/jimm/v3/internal/dbmodel"
jimmm_errors "github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimm"
"github.com/canonical/jimm/v3/internal/jimmhttp/rebac_admin"
"github.com/canonical/jimm/v3/internal/openfga"
ofganames "github.com/canonical/jimm/v3/internal/openfga/names"
"github.com/canonical/jimm/v3/internal/testutils/jimmtest"
"github.com/canonical/jimm/v3/internal/testutils/jimmtest/mocks"
"github.com/canonical/jimm/v3/pkg/api/params"
Expand Down Expand Up @@ -238,3 +240,103 @@ func TestPatchIdentityGroups(t *testing.T) {
_, err = idSvc.PatchIdentityGroups(ctx, "[email protected]", invalidGroupName)
c.Assert(err, qt.ErrorMatches, "Bad Request: ID test-group1 is not a valid group ID")
}

func TestGetIdentityRoles(t *testing.T) {
c := qt.New(t)
var listTuplesErr error
testTuple := openfga.Tuple{
Object: &ofga.Entity{Kind: "user", ID: "foo"},
Relation: ofganames.AssigneeRelation,
Target: &ofga.Entity{Kind: "role", ID: "my-role-id"},
}
roleManager := mocks.RoleManager{
GetRoleByUUID_: func(ctx context.Context, user *openfga.User, uuid string) (*dbmodel.RoleEntry, error) {
return &dbmodel.RoleEntry{Name: "fake-role-name"}, nil
},
}
jimm := jimmtest.JIMM{
FetchIdentity_: func(ctx context.Context, username string) (*openfga.User, error) {
if username == "[email protected]" {
return openfga.NewUser(&dbmodel.Identity{Name: "[email protected]"}, nil), nil
}
return nil, dbmodel.IdentityCreationError
},
RelationService: mocks.RelationService{
ListRelationshipTuples_: func(ctx context.Context, user *openfga.User, tuple params.RelationshipTuple, pageSize int32, continuationToken string) ([]openfga.Tuple, string, error) {
return []openfga.Tuple{testTuple}, "continuation-token", listTuplesErr
},
},
GetRoleManager_: func() jimm.RoleManager {
return roleManager
},
}
user := openfga.User{}
ctx := context.Background()
ctx = rebac_handlers.ContextWithIdentity(ctx, &user)
idSvc := rebac_admin.NewidentitiesService(&jimm)

_, err := idSvc.GetIdentityRoles(ctx, "[email protected]", &resources.GetIdentitiesItemRolesParams{})
c.Assert(err, qt.ErrorMatches, ".*not found")
username := "[email protected]"

res, err := idSvc.GetIdentityRoles(ctx, username, &resources.GetIdentitiesItemRolesParams{})
c.Assert(err, qt.IsNil)
c.Assert(res, qt.IsNotNil)
c.Assert(res.Data, qt.HasLen, 1)
c.Assert(*res.Data[0].Id, qt.Equals, "my-role-id")
c.Assert(res.Data[0].Name, qt.Equals, "fake-role-name")
c.Assert(*res.Next.PageToken, qt.Equals, "continuation-token")

listTuplesErr = errors.New("foo")
_, err = idSvc.GetIdentityRoles(ctx, username, &resources.GetIdentitiesItemRolesParams{})
c.Assert(err, qt.ErrorMatches, "foo")
}

func TestPatchIdentityRoles(t *testing.T) {
c := qt.New(t)
var patchTuplesErr error
jimm := jimmtest.JIMM{
FetchIdentity_: func(ctx context.Context, username string) (*openfga.User, error) {
if username == "[email protected]" {
return openfga.NewUser(&dbmodel.Identity{Name: "[email protected]"}, nil), nil
}
return nil, dbmodel.IdentityCreationError
},
RelationService: mocks.RelationService{
AddRelation_: func(ctx context.Context, user *openfga.User, tuples []params.RelationshipTuple) error {
return patchTuplesErr
},
RemoveRelation_: func(ctx context.Context, user *openfga.User, tuples []params.RelationshipTuple) error {
return patchTuplesErr
},
},
}
user := openfga.User{}
ctx := context.Background()
ctx = rebac_handlers.ContextWithIdentity(ctx, &user)
idSvc := rebac_admin.NewidentitiesService(&jimm)

_, err := idSvc.PatchIdentityRoles(ctx, "[email protected]", nil)
c.Assert(err, qt.ErrorMatches, ".* not found")

username := "[email protected]"
role1ID := uuid.New()
role2ID := uuid.New()
operations := []resources.IdentityRolesPatchItem{
{Role: role1ID.String(), Op: resources.IdentityRolesPatchItemOpAdd},
{Role: role2ID.String(), Op: resources.IdentityRolesPatchItemOpRemove},
}
res, err := idSvc.PatchIdentityRoles(ctx, username, operations)
c.Assert(err, qt.IsNil)
c.Assert(res, qt.IsTrue)

patchTuplesErr = errors.New("foo")
_, err = idSvc.PatchIdentityRoles(ctx, username, operations)
c.Assert(err, qt.ErrorMatches, ".*foo")

invalidRoleName := []resources.IdentityRolesPatchItem{
{Role: "test-role1", Op: resources.IdentityRolesPatchItemOpAdd},
}
_, err = idSvc.PatchIdentityRoles(ctx, "[email protected]", invalidRoleName)
c.Assert(err, qt.ErrorMatches, "Bad Request: ID test-role1 is not a valid role ID")
}
Loading

0 comments on commit 6031feb

Please sign in to comment.