Skip to content

Commit

Permalink
Merge branch 'v3' into internal-jimm-contructor
Browse files Browse the repository at this point in the history
  • Loading branch information
alesstimec authored Dec 2, 2024
2 parents 6a3d9fc + 63be70f commit abc2bf1
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 13 deletions.
1 change: 1 addition & 0 deletions internal/common/pagination/entitlement.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var entitlementResources = []openfga.Kind{
openfga.ModelType,
openfga.ApplicationOfferType,
openfga.GroupType,
openfga.RoleType,
openfga.ServiceAccountType,
}

Expand Down
22 changes: 22 additions & 0 deletions internal/jimmhttp/rebac_admin/capabilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,28 @@ var capabilities = []resources.Capability{
"PATCH",
},
},
{
Endpoint: "/groups/{id}/roles",
Methods: []resources.CapabilityMethods{
"GET",
"PATCH",
},
},
{
Endpoint: "/roles",
Methods: []resources.CapabilityMethods{
"GET",
"POST",
},
},
{
Endpoint: "/roles/{id}",
Methods: []resources.CapabilityMethods{
"GET",
"PUT",
"DELETE",
},
},
{
Endpoint: "/entitlements",
Methods: []resources.CapabilityMethods{
Expand Down
2 changes: 1 addition & 1 deletion internal/jimmhttp/rebac_admin/capabilities_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestCapabilities(t *testing.T) {
defer resp.Body.Close()
// 404 is for not found endpoints and 501 is for "not implemented" endpoints in the rebac-admin-ui-handlers library
isNotFound := resp.StatusCode == 404 || resp.StatusCode == 501
c.Assert(isNotFound, qt.IsFalse)
c.Assert(isNotFound, qt.IsFalse, qt.Commentf("failed for url %s, method %s", url, m))
})

}
Expand Down
11 changes: 11 additions & 0 deletions internal/jimmhttp/rebac_admin/entitlements.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,35 @@ var entitlementsList = []resources.EntitlementSchema{
{Entitlement: "administrator", ReceiverType: "user", EntityType: ApplicationOffer},
{Entitlement: "administrator", ReceiverType: "user:*", EntityType: ApplicationOffer},
{Entitlement: "administrator", ReceiverType: "group#member", EntityType: ApplicationOffer},
{Entitlement: "administrator", ReceiverType: "role#assignee", EntityType: ApplicationOffer},
{Entitlement: "consumer", ReceiverType: "user", EntityType: ApplicationOffer},
{Entitlement: "consumer", ReceiverType: "user:*", EntityType: ApplicationOffer},
{Entitlement: "consumer", ReceiverType: "group#member", EntityType: ApplicationOffer},
{Entitlement: "consumer", ReceiverType: "role#assignee", EntityType: ApplicationOffer},
{Entitlement: "reader", ReceiverType: "user", EntityType: ApplicationOffer},
{Entitlement: "reader", ReceiverType: "user:*", EntityType: ApplicationOffer},
{Entitlement: "reader", ReceiverType: "group#member", EntityType: ApplicationOffer},
{Entitlement: "reader", ReceiverType: "role#assignee", EntityType: ApplicationOffer},

// cloud
{Entitlement: "administrator", ReceiverType: "user", EntityType: Cloud},
{Entitlement: "administrator", ReceiverType: "user:*", EntityType: Cloud},
{Entitlement: "administrator", ReceiverType: "group#member", EntityType: Cloud},
{Entitlement: "administrator", ReceiverType: "role#assignee", EntityType: Cloud},
{Entitlement: "can_addmodel", ReceiverType: "user", EntityType: Cloud},
{Entitlement: "can_addmodel", ReceiverType: "user:*", EntityType: Cloud},
{Entitlement: "can_addmodel", ReceiverType: "group#member", EntityType: Cloud},
{Entitlement: "can_addmodel", ReceiverType: "role#assignee", EntityType: Cloud},

// controller
{Entitlement: "administrator", ReceiverType: "user", EntityType: Controller},
{Entitlement: "administrator", ReceiverType: "user:*", EntityType: Controller},
{Entitlement: "administrator", ReceiverType: "group#member", EntityType: Controller},
{Entitlement: "administrator", ReceiverType: "role#assignee", EntityType: Controller},
{Entitlement: "audit_log_viewer", ReceiverType: "user", EntityType: Controller},
{Entitlement: "audit_log_viewer", ReceiverType: "user:*", EntityType: Controller},
{Entitlement: "audit_log_viewer", ReceiverType: "group#member", EntityType: Controller},
{Entitlement: "audit_log_viewer", ReceiverType: "role#assignee", EntityType: Controller},

// group
{Entitlement: "member", ReceiverType: "user", EntityType: Group},
Expand All @@ -57,17 +64,21 @@ var entitlementsList = []resources.EntitlementSchema{
{Entitlement: "administrator", ReceiverType: "user", EntityType: Model},
{Entitlement: "administrator", ReceiverType: "user:*", EntityType: Model},
{Entitlement: "administrator", ReceiverType: "group#member", EntityType: Model},
{Entitlement: "administrator", ReceiverType: "role#assignee", EntityType: Model},
{Entitlement: "reader", ReceiverType: "user", EntityType: Model},
{Entitlement: "reader", ReceiverType: "user:*", EntityType: Model},
{Entitlement: "reader", ReceiverType: "group#member", EntityType: Model},
{Entitlement: "reader", ReceiverType: "role#assignee", EntityType: Model},
{Entitlement: "writer", ReceiverType: "user", EntityType: Model},
{Entitlement: "writer", ReceiverType: "user:*", EntityType: Model},
{Entitlement: "writer", ReceiverType: "group#member", EntityType: Model},
{Entitlement: "writer", ReceiverType: "role#assignee", EntityType: Model},

// serviceaccount
{Entitlement: "administrator", ReceiverType: "user", EntityType: ServiceAccount},
{Entitlement: "administrator", ReceiverType: "user:*", EntityType: ServiceAccount},
{Entitlement: "administrator", ReceiverType: "group#member", EntityType: ServiceAccount},
{Entitlement: "administrator", ReceiverType: "role#assignee", EntityType: ServiceAccount},
}

// entitlementsService implements the `entitlementsService` interface from rebac-admin-ui-handlers library
Expand Down
4 changes: 2 additions & 2 deletions internal/jimmhttp/rebac_admin/entitlements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ func TestEntitlements(t *testing.T) {
params.Filter = &match
entitlements, err = entitlementSvc.ListEntitlements(ctx, params)
c.Assert(err, qt.IsNil)
c.Assert(entitlements, qt.HasLen, 15)
c.Assert(entitlements, qt.HasLen, 20)

match = "cloud"
params.Filter = &match
entitlements, err = entitlementSvc.ListEntitlements(ctx, params)
c.Assert(err, qt.IsNil)
c.Assert(entitlements, qt.HasLen, 6)
c.Assert(entitlements, qt.HasLen, 8)

match = "#member"
params.Filter = &match
Expand Down
111 changes: 108 additions & 3 deletions internal/jimmhttp/rebac_admin/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimmhttp/rebac_admin/utils"
"github.com/canonical/jimm/v3/internal/jujuapi"
"github.com/canonical/jimm/v3/internal/openfga"
ofganames "github.com/canonical/jimm/v3/internal/openfga/names"
apiparams "github.com/canonical/jimm/v3/pkg/api/params"
jimmnames "github.com/canonical/jimm/v3/pkg/names"
Expand Down Expand Up @@ -236,12 +237,116 @@ func (s *groupsService) PatchGroupIdentities(ctx context.Context, groupId string

// GetGroupRoles returns a page of Roles for Group `groupId`.
func (s *groupsService) GetGroupRoles(ctx context.Context, groupId string, params *resources.GetGroupsItemRolesParams) (*resources.PaginatedResponse[resources.Role], error) {
return nil, v1.NewNotImplementedError("get group roles not implemented")
user, err := utils.GetUserFromContext(ctx)
if err != nil {
return nil, err
}

if !jimmnames.IsValidGroupId(groupId) {
return nil, v1.NewValidationError("invalid group ID")
}

filter := utils.CreateTokenPaginationFilter(params.Size, params.NextToken, params.NextPageToken)
groupTag := jimmnames.NewGroupTag(groupId)
_, err = s.jimm.GetGroupByUUID(ctx, user, groupId)
if err != nil {
if errors.ErrorCode(err) == errors.CodeNotFound {
return nil, v1.NewNotFoundError("group not found")
}
return nil, err
}

tuple := apiparams.RelationshipTuple{
Object: ofganames.WithMemberRelation(groupTag),
Relation: ofganames.AssigneeRelation.String(),
TargetObject: openfga.RoleType.String(),
}
roles, nextToken, err := s.jimm.ListRelationshipTuples(ctx, user, tuple, int32(filter.Limit()), filter.Token()) // #nosec G115 accept integer conversion
if err != nil {
return nil, err
}

data := make([]resources.Role, 0, len(roles))
for _, role := range roles {
roleUUID := role.Target.ID
roleEntry, err := s.jimm.GetRoleManager().GetRoleByUUID(ctx, user, roleUUID)
if err != nil {
// If a role does not exist in the database but a linger tuple exists, drop the role from the results.
if errors.ErrorCode(err) == errors.CodeNotFound {
continue
}
return nil, err
}
data = append(data, resources.Role{
Id: &roleUUID,
Name: roleEntry.Name,
},
)
}

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

// PatchGroupRoles performs addition or removal of a Role to/from a Group identified by `groupId`.
// PatchGroupRoles performs addition or removal of a group to/from a role identified by `groupId`.
func (s *groupsService) PatchGroupRoles(ctx context.Context, groupId string, rolePatches []resources.GroupRolesPatchItem) (bool, error) {
return false, v1.NewNotImplementedError("patch group roles not implemented")
user, err := utils.GetUserFromContext(ctx)
if err != nil {
return false, err
}
if !jimmnames.IsValidGroupId(groupId) {
return false, v1.NewValidationError("invalid group ID")
}

groupTag := jimmnames.NewGroupTag(groupId)
tuple := apiparams.RelationshipTuple{
Object: ofganames.WithMemberRelation(groupTag),
Relation: ofganames.AssigneeRelation.String(),
}

var toRemove []apiparams.RelationshipTuple
var toAdd []apiparams.RelationshipTuple
for _, rolePatch := range rolePatches {
if !jimmnames.IsValidRoleId(rolePatch.Role) {
return false, v1.NewValidationError(fmt.Sprintf("invalid role ID: %s", rolePatch.Role))
}
role := jimmnames.NewRoleTag(rolePatch.Role)
if rolePatch.Op == resources.GroupRolesPatchItemOpAdd {
t := tuple
t.TargetObject = role.String()
toAdd = append(toAdd, t)
} else {
t := tuple
t.TargetObject = role.String()
toRemove = append(toRemove, t)
}
}

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
}

// GetGroupEntitlements returns a page of Entitlements for Group `groupId`.
Expand Down
61 changes: 61 additions & 0 deletions internal/jimmhttp/rebac_admin/groups_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,67 @@ func (s rebacAdminSuite) TestPatchGroupIdentitiesIntegration(c *gc.C) {
c.Assert(allowed, gc.Equals, true)
}

func (s rebacAdminSuite) TestGetGroupRolesIntegration(c *gc.C) {
ctx := context.Background()
group := s.AddGroup(c, "test-group")
role := s.AddRole(c, "test-role")
tuple := openfga.Tuple{
Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag(group.UUID), ofganames.MemberRelation),
Relation: ofganames.AssigneeRelation,
Target: ofganames.ConvertTag(jimmnames.NewRoleTag(role.UUID)),
}
err := s.JIMM.OpenFGAClient.AddRelation(ctx, tuple)
c.Assert(err, gc.IsNil)

params := &resources.GetGroupsItemRolesParams{}
ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
res, err := s.groupSvc.GetGroupRoles(ctx, group.UUID, params)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Not(gc.IsNil))
c.Assert(res.Meta.Size, gc.Equals, 1)
c.Assert(*res.Meta.PageToken, gc.Equals, "")
c.Assert(res.Next.PageToken, gc.IsNil)
c.Assert(res.Data, gc.HasLen, 1)
c.Assert(res.Data[0].Id, gc.Not(gc.IsNil))
c.Assert(*res.Data[0].Id, gc.Equals, role.UUID)
c.Assert(res.Data[0].Name, gc.Equals, role.Name)
}

func (s rebacAdminSuite) TestPatchGroupRolesIntegration(c *gc.C) {
ctx := context.Background()
group := s.AddGroup(c, "test-group")
role := s.AddRole(c, "test-role")

// Assign the role to the group.
rolePatches := []resources.GroupRolesPatchItem{
{Role: role.UUID, Op: resources.GroupRolesPatchItemOpAdd},
}
ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
res, err := s.groupSvc.PatchGroupRoles(ctx, group.UUID, rolePatches)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Equals, true)

checkTuple := openfga.Tuple{
Object: ofganames.ConvertTagWithRelation(group.ResourceTag(), ofganames.MemberRelation),
Relation: ofganames.AssigneeRelation,
Target: ofganames.ConvertTag(role.ResourceTag()),
}
allowed, err := s.JIMM.OpenFGAClient.CheckRelation(ctx, checkTuple, false)
c.Assert(err, gc.IsNil)
c.Assert(allowed, gc.Equals, true)

// Remove the role from the group.
rolePatches[0].Op = resources.GroupRolesPatchItemOpRemove
ctx = rebac_handlers.ContextWithIdentity(ctx, s.AdminUser)
res, err = s.groupSvc.PatchGroupRoles(ctx, group.UUID, rolePatches)
c.Assert(err, gc.IsNil)
c.Assert(res, gc.Equals, true)

allowed, err = s.JIMM.OpenFGAClient.CheckRelation(ctx, checkTuple, false)
c.Assert(err, gc.IsNil)
c.Assert(allowed, gc.Equals, false)
}

func (s rebacAdminSuite) TestGetGroupEntitlementsIntegration(c *gc.C) {
ctx := context.Background()
group, err := s.JIMM.AddGroup(ctx, s.AdminUser, "test-group")
Expand Down
Loading

0 comments on commit abc2bf1

Please sign in to comment.