Skip to content

Commit

Permalink
feat: add identity manager
Browse files Browse the repository at this point in the history
  • Loading branch information
kian99 committed Dec 20, 2024
1 parent b480a9a commit 70870c8
Show file tree
Hide file tree
Showing 13 changed files with 284 additions and 153 deletions.
65 changes: 0 additions & 65 deletions internal/jimm/identity.go

This file was deleted.

5 changes: 5 additions & 0 deletions internal/jimm/identity/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Copyright 2024 Canonical.
package identity

// Identity is a type alias to export identityManager for use in tests.
type IdentityManager = identityManager
82 changes: 82 additions & 0 deletions internal/jimm/identity/identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2024 Canonical.
package identity

import (
"context"

"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"
)

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

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

// FetchIdentity fetches the user specified by the username and returns the user if it is found.
// Or error "record not found".
func (j *identityManager) FetchIdentity(ctx context.Context, id string) (*openfga.User, error) {
const op = errors.Op("jimm.FetchIdentity")

identity, err := dbmodel.NewIdentity(id)
if err != nil {
return nil, errors.E(op, err)
}

if err := j.store.FetchIdentity(ctx, identity); err != nil {
return nil, err
}
u := openfga.NewUser(identity, j.authSvc)

return u, nil
}

// ListIdentities lists a page of users in our database and parse them into openfga entities.
// `match` will filter the list for fuzzy find on identity name.
func (j *identityManager) ListIdentities(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) {
const op = errors.Op("jimm.ListIdentities")

if !user.JimmAdmin {
return nil, errors.E(op, errors.CodeUnauthorized, "unauthorized")
}
identities, err := j.store.ListIdentities(ctx, pagination.Limit(), pagination.Offset(), match)
var users []openfga.User

for _, id := range identities {
users = append(users, *openfga.NewUser(&id, j.authSvc))
}
if err != nil {
return nil, errors.E(op, err)
}
return users, nil
}

// CountIdentities returns the count of all the identities in our database.
func (j *identityManager) CountIdentities(ctx context.Context, user *openfga.User) (int, error) {
const op = errors.Op("jimm.CountIdentities")

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

count, err := j.store.CountIdentities(ctx)
if err != nil {
return 0, errors.E(op, err)
}
return count, nil
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,76 @@
// Copyright 2024 Canonical.

package jimm_test
package identity_test

import (
"context"
"testing"
"time"

qt "github.com/frankban/quicktest"
"github.com/frankban/quicktest/qtsuite"

"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/jimm/identity"
"github.com/canonical/jimm/v3/internal/openfga"
"github.com/canonical/jimm/v3/internal/testutils/jimmtest"
)

func TestFetchIdentity(t *testing.T) {
c := qt.New(t)
ctx := context.Background()
type identityManagerSuite struct {
manager *identity.IdentityManager
adminUser *openfga.User
db *db.Database
ofgaClient *openfga.OFGAClient
}

func (s *identityManagerSuite) Init(c *qt.C) {
// Setup DB
db := &db.Database{
DB: jimmtest.PostgresDB(c, time.Now),
}
err := db.Migrate(context.Background(), false)
c.Assert(err, qt.IsNil)

j := jimmtest.NewJIMM(c, nil)
s.db = db

user, _, _, _, _, _, _, _ := jimmtest.CreateTestControllerEnvironment(ctx, c, j.Database)
u, err := j.FetchIdentity(ctx, user.Name)
// Setup OFGA
ofgaClient, _, _, err := jimmtest.SetupTestOFGAClient(c.Name())
c.Assert(err, qt.IsNil)
c.Assert(u.Name, qt.Equals, user.Name)

_, err = j.FetchIdentity(ctx, "bobnotfound@canonical.com")
c.Assert(err, qt.ErrorMatches, "record not found")
s.ofgaClient = ofgaClient

s.manager, err = identity.NewIdentityManager(db, ofgaClient)
c.Assert(err, qt.IsNil)

// Create test identity
i, err := dbmodel.NewIdentity("alice")
c.Assert(err, qt.IsNil)
s.adminUser = openfga.NewUser(i, ofgaClient)
s.adminUser.JimmAdmin = true
}

func TestListIdentities(t *testing.T) {
c := qt.New(t)
func (s *identityManagerSuite) TestFetchIdentity(c *qt.C) {
c.Parallel()
ctx := context.Background()

j := jimmtest.NewJIMM(c, nil)
identity := dbmodel.Identity{Name: "fake-name"}
err := s.db.GetIdentity(ctx, &identity)
c.Assert(err, qt.IsNil)
u, err := s.manager.FetchIdentity(ctx, identity.Name)
c.Assert(err, qt.IsNil)
c.Assert(u.Name, qt.Equals, identity.Name)

_, err = s.manager.FetchIdentity(ctx, "bobnotfound@canonical.com")
c.Assert(err, qt.ErrorMatches, "record not found")
}

u := openfga.NewUser(&dbmodel.Identity{Name: "admin@canonical.com"}, j.OpenFGAClient)
u.JimmAdmin = true
func (s *identityManagerSuite) TestListIdentities(c *qt.C) {
c.Parallel()
ctx := context.Background()

pag := pagination.NewOffsetFilter(10, 0)
users, err := j.ListIdentities(ctx, u, pag, "")
users, err := s.manager.ListIdentities(ctx, s.adminUser, pag, "")
c.Assert(err, qt.IsNil)
c.Assert(len(users), qt.Equals, 0)

Expand All @@ -51,7 +82,8 @@ func TestListIdentities(t *testing.T) {
}
// add users
for _, name := range userNames {
_, err := j.GetUser(ctx, name)
identity := dbmodel.Identity{Name: name}
err := s.db.GetIdentity(ctx, &identity)
c.Assert(err, qt.IsNil)
}

Expand Down Expand Up @@ -91,7 +123,7 @@ func TestListIdentities(t *testing.T) {
for _, t := range testCases {
c.Run(t.desc, func(c *qt.C) {
pag = pagination.NewOffsetFilter(t.limit, t.offset)
identities, err := j.ListIdentities(ctx, u, pag, t.match)
identities, err := s.manager.ListIdentities(ctx, s.adminUser, pag, t.match)
c.Assert(err, qt.IsNil)
c.Assert(identities, qt.HasLen, len(t.identities))
for i := range len(t.identities) {
Expand All @@ -101,15 +133,10 @@ func TestListIdentities(t *testing.T) {
}
}

func TestCountIdentities(t *testing.T) {
c := qt.New(t)
func (s *identityManagerSuite) TestCountIdentities(c *qt.C) {
c.Parallel()
ctx := context.Background()

j := jimmtest.NewJIMM(c, nil)

u := openfga.NewUser(&dbmodel.Identity{Name: "admin@canonical.com"}, j.OpenFGAClient)
u.JimmAdmin = true

userNames := []string{
"bob1@canonical.com",
"bob3@canonical.com",
Expand All @@ -118,10 +145,15 @@ func TestCountIdentities(t *testing.T) {
}
// add users
for _, name := range userNames {
_, err := j.GetUser(ctx, name)
identity := dbmodel.Identity{Name: name}
err := s.db.GetIdentity(ctx, &identity)
c.Assert(err, qt.IsNil)
}
count, err := j.CountIdentities(ctx, u)
count, err := s.manager.CountIdentities(ctx, s.adminUser)
c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 4)
}

func TestIdentityManager(t *testing.T) {
qtsuite.Run(qt.New(t), &identityManagerSuite{})
}
23 changes: 21 additions & 2 deletions internal/jimm/jimm.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/canonical/jimm/v3/internal/errors"
"github.com/canonical/jimm/v3/internal/jimm/credentials"
"github.com/canonical/jimm/v3/internal/jimm/group"
"github.com/canonical/jimm/v3/internal/jimm/identity"
"github.com/canonical/jimm/v3/internal/jimm/role"
"github.com/canonical/jimm/v3/internal/jimmjwx"
"github.com/canonical/jimm/v3/internal/openfga"
Expand Down Expand Up @@ -140,6 +141,12 @@ type GroupManager interface {
CountGroups(ctx context.Context, user *openfga.User) (int, error)
}

type IdentityManager interface {
FetchIdentity(ctx context.Context, id string) (*openfga.User, error)
ListIdentities(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error)
CountIdentities(ctx context.Context, user *openfga.User) (int, error)
}

// Parameters holds the services and static fields passed to the jimm.New() constructor.
// You can provide mock implementations of certain services where necessary for dependency injection.
type Parameters struct {
Expand Down Expand Up @@ -235,18 +242,24 @@ func New(p Parameters) (*JIMM, error) {
return nil, errors.E(err)
}

roleManager, err := role.NewRoleManager(j.Database, p.OpenFGAClient)
roleManager, err := role.NewRoleManager(j.Database, j.OpenFGAClient)
if err != nil {
return nil, err
}
j.roleManager = roleManager

groupManager, err := group.NewGroupManager(j.Database, p.OpenFGAClient)
groupManager, err := group.NewGroupManager(j.Database, j.OpenFGAClient)
if err != nil {
return nil, err
}
j.groupManager = groupManager

identityManager, err := identity.NewIdentityManager(j.Database, j.OpenFGAClient)
if err != nil {
return nil, err
}
j.identityManager = identityManager

return j, nil
}

Expand All @@ -262,6 +275,8 @@ type JIMM struct {

// groupManager provides a means to manage groups within JIMM.
groupManager GroupManager

identityManager IdentityManager
}

// ResourceTag returns JIMM's controller tag stating its UUID.
Expand All @@ -284,6 +299,10 @@ func (j *JIMM) GroupManager() GroupManager {
return j.groupManager
}

func (j *JIMM) IdentityManager() IdentityManager {
return j.identityManager
}

type permission struct {
resource string
relation string
Expand Down
Loading

0 comments on commit 70870c8

Please sign in to comment.