Skip to content

Commit

Permalink
Juju 7124/implement jimm role package (#1455)
Browse files Browse the repository at this point in the history
* add role to parse tag

* feat(db): update db with roles

Updates db with roles entries.

* test(roleentry): adds some basic tests

* feat(database): addRole db method

* feat(dbmodel): migration was missing comma

* fix(schema-version): missing schema version in migration caused infinite migration loop

* feat(database): adds database methods for roles according to the specification

* feat(database): use List and Count for roles

* chore(testname): fix test name for groups

* feat(internal/openfga): add roles to internal/openfga

Allows the removal of a role from OpenFGA and plugs them into resourceTypes, adds their relation
constant, and tests we can relate users->role, user->group--group#member->assignee->role

* docs(removerole godoc): remove role godoc update

* feat(rolemanager): implements RoleManager

* feat(rolemanagertest): update rolemanager test jimmtest ref

* chore(pr): cleanup interfaces and use concrete types

---------

Co-authored-by: SimoneDutto <simone.dutto@canonical.com>
  • Loading branch information
ale8k and SimoneDutto authored Nov 25, 2024
1 parent 8e8fef3 commit cb701b8
Show file tree
Hide file tree
Showing 5 changed files with 435 additions and 0 deletions.
7 changes: 7 additions & 0 deletions cmd/jimmsrv/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimm"
jimmcreds "github.com/canonical/jimm/v3/internal/jimm/credentials"
"github.com/canonical/jimm/v3/internal/jimm/role"
"github.com/canonical/jimm/v3/internal/jimmhttp"
"github.com/canonical/jimm/v3/internal/jimmhttp/rebac_admin"
"github.com/canonical/jimm/v3/internal/jimmjwx"
Expand Down Expand Up @@ -398,6 +399,12 @@ func NewService(ctx context.Context, p Params) (*Service, error) {
JWTService: s.jimm.JWTService,
}

roleManager, err := role.NewRoleManager(&s.jimm.Database, s.jimm.OpenFGAClient)
if err != nil {
return nil, errors.E(op, err, "failed to create RoleManager")
}
s.jimm.RoleManager = roleManager

if !p.DisableConnectionCache {
s.jimm.Dialer = jimm.CacheDialer(s.jimm.Dialer)
}
Expand Down
23 changes: 23 additions & 0 deletions internal/jimm/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/sync/errgroup"

"github.com/canonical/jimm/v3/internal/common/pagination"
"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/dbmodel"
"github.com/canonical/jimm/v3/internal/errors"
Expand Down Expand Up @@ -86,6 +87,28 @@ type JIMM struct {
// OAuthAuthenticator is responsible for handling authentication
// via OAuth2.0 AND JWT access tokens to JIMM.
OAuthAuthenticator OAuthAuthenticator

// RoleManager provides a means to manage roles within JIMM.
RoleManager RoleManager
}

// RoleManager provides a means to manage roles within JIMM.
type RoleManager interface {
// AddRole adds a role to JIMM.
AddRole(ctx context.Context, user *openfga.User, roleName string) (*dbmodel.RoleEntry, error)
// GetRoleByUUID returns a role based on the provided UUID.
GetRoleByUUID(ctx context.Context, user *openfga.User, uuid string) (*dbmodel.RoleEntry, error)
// GetRoleByName returns a role based on the provided name.
GetRoleByName(ctx context.Context, user *openfga.User, name string) (*dbmodel.RoleEntry, error)
// RemoveRole removes the role from JIMM in both the store and authorisation store.
RemoveRole(ctx context.Context, user *openfga.User, roleName string) error
// RenameRole renames a role in JIMM's DB.
RenameRole(ctx context.Context, user *openfga.User, uuid, newName string) error
// ListRoles returns a list of roles known to JIMM.
// `match` will filter the list fuzzy matching role's name or uuid.
ListRoles(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.RoleEntry, error)
// CountRoles returns the number of roles that exist.
CountRoles(ctx context.Context, user *openfga.User) (int, error)
}

// ResourceTag returns JIMM's controller tag stating its UUID.
Expand Down
6 changes: 6 additions & 0 deletions internal/jimm/role/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright 2024 Canonical.

package role

// RoleManager is a type alias to export roleManager for use in tests.
type RoleManager = roleManager
161 changes: 161 additions & 0 deletions internal/jimm/role/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2024 Canonical.

package role

import (
"context"
"fmt"

"github.com/juju/zaputil/zapctx"

"github.com/canonical/jimm/v3/internal/common/pagination"
"github.com/canonical/jimm/v3/internal/db"
"github.com/canonical/jimm/v3/internal/dbmodel"
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/openfga"
)

// roleManager provides a means to manage roles within JIMM.
type roleManager struct {
store *db.Database
authSvc *openfga.OFGAClient
}

// NewRoleManager returns a new RoleManager that persists the roles in the provided store.
func NewRoleManager(store *db.Database, authSvc *openfga.OFGAClient) (*roleManager, error) {
if store == nil {
return nil, errors.E("role store cannot be nil")
}
if authSvc == nil {
return nil, errors.E("role authorisation service cannot be nil")
}
return &roleManager{store, authSvc}, nil
}

// AddRole adds a role to JIMM.
func (rm *roleManager) AddRole(ctx context.Context, user *openfga.User, roleName string) (*dbmodel.RoleEntry, error) {
const op = errors.Op("roleManager.AddRole")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

re, err := rm.store.AddRole(ctx, roleName)
if err != nil {
return nil, errors.E(op, err)
}
return re, nil
}

// GetRoleByUUID returns a role based on the provided UUID.
func (rm *roleManager) GetRoleByUUID(ctx context.Context, user *openfga.User, uuid string) (*dbmodel.RoleEntry, error) {
const op = errors.Op("roleManager.GetRoleByUUID")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

return rm.getRole(ctx, user, &dbmodel.RoleEntry{UUID: uuid})
}

// GetRoleByName returns a role based on the provided name.
func (rm *roleManager) GetRoleByName(ctx context.Context, user *openfga.User, name string) (*dbmodel.RoleEntry, error) {
const op = errors.Op("roleManager.GetRoleByName")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

return rm.getRole(ctx, user, &dbmodel.RoleEntry{Name: name})
}

// RemoveRole removes the role from JIMM in both the store and authorisation store.
func (rm *roleManager) RemoveRole(ctx context.Context, user *openfga.User, roleName string) error {
const op = errors.Op("roleManager.RemoveRole")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

re := &dbmodel.RoleEntry{
Name: roleName,
}
err := rm.store.GetRole(ctx, re)
if err != nil {
return errors.E(op, err)
}

// TODO(ale8k):
// Would be nice to have a way to create a transaction to get, remove tuples, if successful, delete role
// somehow. We could pass a callback and change the db methods?
if err := rm.authSvc.RemoveRole(ctx, re.ResourceTag()); err != nil {
return errors.E(op, err)
}

if err := rm.store.RemoveRole(ctx, re); err != nil {
return errors.E(op, err)
}

return nil
}

// RenameRole renames a role in JIMM's DB.
func (rm *roleManager) RenameRole(ctx context.Context, user *openfga.User, uuid, newName string) error {
const op = errors.Op("roleManager.RenameRole")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

err := rm.store.UpdateRoleName(ctx, uuid, newName)
if err != nil {
return errors.E(op, err)
}

return nil
}

// ListRoles returns a list of roles known to JIMM.
// `match` will filter the list fuzzy matching role's name or uuid.
func (rm *roleManager) ListRoles(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]dbmodel.RoleEntry, error) {
const op = errors.Op("roleManager.ListRoles")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

res, err := rm.store.ListRoles(ctx, pagination.Limit(), pagination.Offset(), match)
if err != nil {
return nil, errors.E(op, err)
}
return res, nil
}

// CountRoles returns the number of roles that exist.
func (rm *roleManager) CountRoles(ctx context.Context, user *openfga.User) (int, error) {
const op = errors.Op("roleManager.CountRoles")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return 0, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}
count, err := rm.store.CountRoles(ctx)
if err != nil {
return 0, errors.E(op, err)
}
return count, nil
}

// getRole returns a role based on the provided UUID or name.
func (rm *roleManager) getRole(ctx context.Context, user *openfga.User, role *dbmodel.RoleEntry) (*dbmodel.RoleEntry, error) {
const op = errors.Op("roleManager.getRole")
zapctx.Debug(ctx, fmt.Sprintf("Running %s", op))

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}

if err := rm.store.GetRole(ctx, role); err != nil {
return nil, errors.E(op, err)
}

return role, nil
}
Loading

0 comments on commit cb701b8

Please sign in to comment.