diff --git a/internal/jimm/identity.go b/internal/jimm/identity.go deleted file mode 100644 index 6119cfc13..000000000 --- a/internal/jimm/identity.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2024 Canonical. - -package jimm - -import ( - "context" - - "github.com/canonical/jimm/v3/internal/common/pagination" - "github.com/canonical/jimm/v3/internal/dbmodel" - "github.com/canonical/jimm/v3/internal/errors" - "github.com/canonical/jimm/v3/internal/openfga" -) - -// FetchIdentity fetches the user specified by the username and returns the user if it is found. -// Or error "record not found". -func (j *JIMM) 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.Database.FetchIdentity(ctx, identity); err != nil { - return nil, err - } - u := openfga.NewUser(identity, j.OpenFGAClient) - - 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 *JIMM) 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.Database.ListIdentities(ctx, pagination.Limit(), pagination.Offset(), match) - var users []openfga.User - - for _, id := range identities { - users = append(users, *openfga.NewUser(&id, j.OpenFGAClient)) - } - 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 *JIMM) 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.Database.CountIdentities(ctx) - if err != nil { - return 0, errors.E(op, err) - } - return count, nil -} diff --git a/internal/jimm/identity/export_test.go b/internal/jimm/identity/export_test.go new file mode 100644 index 000000000..97b3d5212 --- /dev/null +++ b/internal/jimm/identity/export_test.go @@ -0,0 +1,6 @@ +// Copyright 2025 Canonical. + +package identity + +// Identity is a type alias to export identityManager for use in tests. +type IdentityManager = identityManager diff --git a/internal/jimm/identity/identity.go b/internal/jimm/identity/identity.go new file mode 100644 index 000000000..e446e7a68 --- /dev/null +++ b/internal/jimm/identity/identity.go @@ -0,0 +1,82 @@ +// Copyright 2025 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 + } + + return openfga.NewUser(identity, j.authSvc), 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 +} diff --git a/internal/jimm/identity_test.go b/internal/jimm/identity/identity_test.go similarity index 50% rename from internal/jimm/identity_test.go rename to internal/jimm/identity/identity_test.go index ea6c08ef0..ad34222b9 100644 --- a/internal/jimm/identity_test.go +++ b/internal/jimm/identity/identity_test.go @@ -1,45 +1,77 @@ -// Copyright 2024 Canonical. +// Copyright 2025 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()) + 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) @@ -51,7 +83,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) } @@ -91,7 +124,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) { @@ -101,15 +134,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", @@ -118,10 +146,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{}) +} diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 3f4e36a0c..aad925ec1 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -1,4 +1,4 @@ -// Copyright 2024 Canonical. +// Copyright 2025 Canonical. // Package jimm contains the business logic used to manage clouds, // cloudcredentials and models. @@ -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" @@ -140,6 +141,14 @@ type GroupManager interface { CountGroups(ctx context.Context, user *openfga.User) (int, error) } +// IdentityManager provides a means to fetch identities in JIMM. +// Identities cannot be created here, that can only be done via login. +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 { @@ -235,18 +244,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 } @@ -262,6 +277,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. @@ -284,6 +301,11 @@ func (j *JIMM) GroupManager() GroupManager { return j.groupManager } +// IdentityManager returns a manager that enables identity (user/service-account) management. +func (j *JIMM) IdentityManager() IdentityManager { + return j.identityManager +} + type permission struct { resource string relation string diff --git a/internal/jimmhttp/rebac_admin/capabilities_test.go b/internal/jimmhttp/rebac_admin/capabilities_test.go index dc0aac495..22bb00f88 100644 --- a/internal/jimmhttp/rebac_admin/capabilities_test.go +++ b/internal/jimmhttp/rebac_admin/capabilities_test.go @@ -11,14 +11,27 @@ import ( qt "github.com/frankban/quicktest" + "github.com/canonical/jimm/v3/internal/dbmodel" + "github.com/canonical/jimm/v3/internal/jimm" "github.com/canonical/jimm/v3/internal/jimmhttp/rebac_admin" + "github.com/canonical/jimm/v3/internal/openfga" "github.com/canonical/jimm/v3/internal/testutils/jimmtest" + "github.com/canonical/jimm/v3/internal/testutils/jimmtest/mocks" ) // test capabilities are reachable func TestCapabilities(t *testing.T) { c := qt.New(t) - jimm := jimmtest.JIMM{} + identityManager := mocks.IdentityManager{ + FetchIdentity_: func(ctx context.Context, id string) (*openfga.User, error) { + return openfga.NewUser(&dbmodel.Identity{Name: id}, nil), nil + }, + } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, + } ctx := context.Background() handlers, err := rebac_admin.SetupBackend(ctx, &jimm) c.Assert(err, qt.IsNil) diff --git a/internal/jimmhttp/rebac_admin/identities.go b/internal/jimmhttp/rebac_admin/identities.go index c01acc363..709c6e4da 100644 --- a/internal/jimmhttp/rebac_admin/identities.go +++ b/internal/jimmhttp/rebac_admin/identities.go @@ -39,7 +39,7 @@ func (s *identitiesService) ListIdentities(ctx context.Context, params *resource return nil, err } - count, err := s.jimm.CountIdentities(ctx, user) + count, err := s.jimm.IdentityManager().CountIdentities(ctx, user) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (s *identitiesService) ListIdentities(ctx context.Context, params *resource if params.Filter != nil && *params.Filter != "" { match = *params.Filter } - users, err := s.jimm.ListIdentities(ctx, user, pagination, match) + users, err := s.jimm.IdentityManager().ListIdentities(ctx, user, pagination, match) if err != nil { return nil, err } @@ -77,7 +77,7 @@ func (s *identitiesService) CreateIdentity(ctx context.Context, identity *resour // GetIdentity returns a single Identity. func (s *identitiesService) GetIdentity(ctx context.Context, identityId string) (*resources.Identity, error) { - user, err := s.jimm.FetchIdentity(ctx, identityId) + user, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { if errors.ErrorCode(err) == errors.CodeNotFound { return nil, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) @@ -104,7 +104,7 @@ func (s *identitiesService) GetIdentityRoles(ctx context.Context, identityId str if err != nil { return nil, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { return nil, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) } @@ -156,7 +156,7 @@ func (s *identitiesService) PatchIdentityRoles(ctx context.Context, identityId s return false, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { return false, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) } @@ -200,7 +200,7 @@ func (s *identitiesService) GetIdentityGroups(ctx context.Context, identityId st if err != nil { return nil, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { return nil, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) } @@ -252,7 +252,7 @@ func (s *identitiesService) PatchIdentityGroups(ctx context.Context, identityId return false, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { return false, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) } @@ -296,7 +296,7 @@ func (s *identitiesService) GetIdentityEntitlements(ctx context.Context, identit if err != nil { return nil, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { if errors.ErrorCode(err) == errors.CodeNotFound { return nil, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) @@ -333,7 +333,7 @@ func (s *identitiesService) PatchIdentityEntitlements(ctx context.Context, ident if err != nil { return false, err } - objUser, err := s.jimm.FetchIdentity(ctx, identityId) + objUser, err := s.jimm.IdentityManager().FetchIdentity(ctx, identityId) if err != nil { return false, v1.NewNotFoundError(fmt.Sprintf("User with id %s not found", identityId)) } diff --git a/internal/jimmhttp/rebac_admin/identities_integration_test.go b/internal/jimmhttp/rebac_admin/identities_integration_test.go index 04343a52d..5a00dedb2 100644 --- a/internal/jimmhttp/rebac_admin/identities_integration_test.go +++ b/internal/jimmhttp/rebac_admin/identities_integration_test.go @@ -74,7 +74,7 @@ func (s *identitiesSuite) TestIdentityPatchGroups(c *gc.C) { c.Assert(changed, gc.Equals, true) // test user added to groups - objUser, err := s.JIMM.FetchIdentity(ctx, username) + objUser, err := s.JIMM.IdentityManager().FetchIdentity(ctx, username) c.Assert(err, gc.IsNil) tuples, _, err := s.JIMM.ListRelationshipTuples(ctx, s.AdminUser, params.RelationshipTuple{ Object: objUser.ResourceTag().String(), @@ -204,7 +204,7 @@ func (s *identitiesSuite) TestIdentityPatchRoles(c *gc.C) { c.Assert(changed, gc.Equals, true) // test user added to roles - objUser, err := s.JIMM.FetchIdentity(ctx, username) + objUser, err := s.JIMM.IdentityManager().FetchIdentity(ctx, username) c.Assert(err, gc.IsNil) tuples, _, err := s.JIMM.ListRelationshipTuples(ctx, s.AdminUser, params.RelationshipTuple{ Object: objUser.ResourceTag().String(), diff --git a/internal/jimmhttp/rebac_admin/identities_test.go b/internal/jimmhttp/rebac_admin/identities_test.go index 13bf66a6e..0b9a068b5 100644 --- a/internal/jimmhttp/rebac_admin/identities_test.go +++ b/internal/jimmhttp/rebac_admin/identities_test.go @@ -28,7 +28,7 @@ import ( func TestGetIdentity(t *testing.T) { c := qt.New(t) - jimm := jimmtest.JIMM{ + identityManager := mocks.IdentityManager{ FetchIdentity_: func(ctx context.Context, username string) (*openfga.User, error) { if username == "bob@canonical.com" { return openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, nil), nil @@ -36,6 +36,11 @@ func TestGetIdentity(t *testing.T) { return nil, jimmm_errors.E(jimmm_errors.CodeNotFound) }, } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, + } user := openfga.User{} user.JimmAdmin = true ctx := context.Background() @@ -61,7 +66,7 @@ func TestListIdentities(t *testing.T) { *openfga.NewUser(&dbmodel.Identity{Name: "bob4@canonical.com"}, nil), } c := qt.New(t) - jimm := jimmtest.JIMM{ + identityManager := mocks.IdentityManager{ ListIdentities_: func(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) { start := pagination.Offset() end := start + pagination.Limit() @@ -74,6 +79,11 @@ func TestListIdentities(t *testing.T) { return len(testUsers), nil }, } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, + } user := openfga.User{} user.JimmAdmin = true ctx := context.Background() @@ -157,13 +167,18 @@ func TestGetIdentityGroups(t *testing.T) { return &dbmodel.GroupEntry{Name: "fake-group-name"}, nil }, } - jimm := jimmtest.JIMM{ - FetchIdentity_: func(ctx context.Context, username string) (*openfga.User, error) { - if username == "bob@canonical.com" { + identityManager := mocks.IdentityManager{ + FetchIdentity_: func(ctx context.Context, id string) (*openfga.User, error) { + if id == "bob@canonical.com" { return openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, nil), nil } return nil, dbmodel.IdentityCreationError }, + } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, 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 @@ -198,13 +213,18 @@ func TestGetIdentityGroups(t *testing.T) { func TestPatchIdentityGroups(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 == "bob@canonical.com" { + identityManager := mocks.IdentityManager{ + FetchIdentity_: func(ctx context.Context, id string) (*openfga.User, error) { + if id == "bob@canonical.com" { return openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, nil), nil } return nil, dbmodel.IdentityCreationError }, + } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, RelationService: mocks.RelationService{ AddRelation_: func(ctx context.Context, user *openfga.User, tuples []params.RelationshipTuple) error { return patchTuplesErr @@ -257,13 +277,18 @@ func TestGetIdentityRoles(t *testing.T) { return &dbmodel.RoleEntry{Name: "fake-role-name"}, nil }, } - jimm := jimmtest.JIMM{ - FetchIdentity_: func(ctx context.Context, username string) (*openfga.User, error) { - if username == "bob@canonical.com" { + identityManager := mocks.IdentityManager{ + FetchIdentity_: func(ctx context.Context, id string) (*openfga.User, error) { + if id == "bob@canonical.com" { return openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, nil), nil } return nil, dbmodel.IdentityCreationError }, + } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, 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 @@ -298,13 +323,18 @@ func TestGetIdentityRoles(t *testing.T) { 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 == "bob@canonical.com" { + identityManager := mocks.IdentityManager{ + FetchIdentity_: func(ctx context.Context, id string) (*openfga.User, error) { + if id == "bob@canonical.com" { return openfga.NewUser(&dbmodel.Identity{Name: "bob@canonical.com"}, nil), nil } return nil, dbmodel.IdentityCreationError }, + } + jimm := jimmtest.JIMM{ + IdentityManager_: func() jimm.IdentityManager { + return &identityManager + }, RelationService: mocks.RelationService{ AddRelation_: func(ctx context.Context, user *openfga.User, tuples []params.RelationshipTuple) error { return patchTuplesErr diff --git a/internal/jujuapi/interface.go b/internal/jujuapi/interface.go index a9bfc03dd..9a5d86ac1 100644 --- a/internal/jujuapi/interface.go +++ b/internal/jujuapi/interface.go @@ -32,7 +32,6 @@ type JIMM interface { AddHostedCloud(ctx context.Context, user *openfga.User, tag names.CloudTag, cloud jujuparams.Cloud, force bool) error AddServiceAccount(ctx context.Context, u *openfga.User, clientId string) error CopyServiceAccountCredential(ctx context.Context, u *openfga.User, svcAcc *openfga.User, cloudCredentialTag names.CloudCredentialTag) (names.CloudCredentialTag, []jujuparams.UpdateCredentialModelResult, error) - CountIdentities(ctx context.Context, user *openfga.User) (int, error) DestroyOffer(ctx context.Context, user *openfga.User, offerURL string, force bool) error FindApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) @@ -46,9 +45,8 @@ type JIMM interface { GetCloudCredentialAttributes(ctx context.Context, u *openfga.User, cred *dbmodel.CloudCredential, hidden bool) (attrs map[string]string, redacted []string, err error) RoleManager() jimm.RoleManager GroupManager() jimm.GroupManager + IdentityManager() jimm.IdentityManager GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) - // FetchIdentity finds the user in jimm or returns a not-found error - FetchIdentity(ctx context.Context, username string) (*openfga.User, error) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) GetUserControllerAccess(ctx context.Context, user *openfga.User, controller names.ControllerTag) (string, error) GetUserModelAccess(ctx context.Context, user *openfga.User, model names.ModelTag) (string, error) @@ -60,7 +58,6 @@ type JIMM interface { InitiateInternalMigration(ctx context.Context, user *openfga.User, modelNameOrUUID string, targetController string) (jujuparams.InitiateMigrationResult, error) InitiateMigration(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) - ListIdentities(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) ListModels(ctx context.Context, user *openfga.User) ([]base.UserModel, error) ListResources(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination, namePrefixFilter, typeFilter string) ([]db.Resource, error) Offer(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error diff --git a/internal/testutils/jimmtest/jimm_mock.go b/internal/testutils/jimmtest/jimm_mock.go index 0b61fa46c..17d1a8aaf 100644 --- a/internal/testutils/jimmtest/jimm_mock.go +++ b/internal/testutils/jimmtest/jimm_mock.go @@ -41,9 +41,7 @@ type JIMM struct { Authenticate_ func(ctx context.Context, req *jujuparams.LoginRequest) (*openfga.User, error) CheckPermission_ func(ctx context.Context, user *openfga.User, cachedPerms map[string]string, desiredPerms map[string]interface{}) (map[string]string, error) CopyServiceAccountCredential_ func(ctx context.Context, u *openfga.User, svcAcc *openfga.User, cloudCredentialTag names.CloudCredentialTag) (names.CloudCredentialTag, []jujuparams.UpdateCredentialModelResult, error) - CountIdentities_ func(ctx context.Context, user *openfga.User) (int, error) DestroyOffer_ func(ctx context.Context, user *openfga.User, offerURL string, force bool) error - FetchIdentity_ func(ctx context.Context, username string) (*openfga.User, error) FindApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) FindAuditEvents_ func(ctx context.Context, user *openfga.User, filter db.AuditLogFilter) ([]dbmodel.AuditLogEntry, error) ForEachCloud_ func(ctx context.Context, user *openfga.User, f func(*dbmodel.Cloud) error) error @@ -65,10 +63,10 @@ type JIMM struct { GrantOfferAccess_ func(ctx context.Context, u *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) error GrantServiceAccountAccess_ func(ctx context.Context, u *openfga.User, svcAccTag jimmnames.ServiceAccountTag, entities []string) error GroupManager_ func() jimm.GroupManager + IdentityManager_ func() jimm.IdentityManager InitiateInternalMigration_ func(ctx context.Context, user *openfga.User, modelNameOrUUID string, targetController string) (jujuparams.InitiateMigrationResult, error) InitiateMigration_ func(ctx context.Context, user *openfga.User, spec jujuparams.MigrationSpec) (jujuparams.InitiateMigrationResult, error) ListApplicationOffers_ func(ctx context.Context, user *openfga.User, filters ...jujuparams.OfferFilter) ([]jujuparams.ApplicationOfferAdminDetailsV5, error) - ListIdentities_ func(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) ListResources_ func(ctx context.Context, user *openfga.User, filter pagination.LimitOffsetPagination, namePrefixFilter, typeFilter string) ([]db.Resource, error) Offer_ func(ctx context.Context, user *openfga.User, offer jimm.AddApplicationOfferParams) error PubSubHub_ func() *pubsub.Hub @@ -82,7 +80,6 @@ type JIMM struct { RevokeModelAccess_ func(ctx context.Context, user *openfga.User, mt names.ModelTag, ut names.UserTag, access jujuparams.UserAccessPermission) error RevokeOfferAccess_ func(ctx context.Context, user *openfga.User, offerURL string, ut names.UserTag, access jujuparams.OfferAccessPermission) (err error) RoleManager_ func() jimm.RoleManager - SetIdentityModelDefaults_ func(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error ToJAASTag_ func(ctx context.Context, tag *ofganames.Tag, resolveUUIDs bool) (string, error) UpdateApplicationOffer_ func(ctx context.Context, controller *dbmodel.Controller, offerUUID string, removed bool) error UpdateCloud_ func(ctx context.Context, u *openfga.User, ct names.CloudTag, cloud jujuparams.Cloud) error @@ -227,30 +224,19 @@ func (j *JIMM) GroupManager() jimm.GroupManager { return j.GroupManager_() } +func (j *JIMM) IdentityManager() jimm.IdentityManager { + if j.IdentityManager_ == nil { + return nil + } + return j.IdentityManager_() +} + func (j *JIMM) GetJimmControllerAccess(ctx context.Context, user *openfga.User, tag names.UserTag) (string, error) { if j.GetJimmControllerAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) } return j.GetJimmControllerAccess_(ctx, user, tag) } -func (j *JIMM) FetchIdentity(ctx context.Context, username string) (*openfga.User, error) { - if j.FetchIdentity_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.FetchIdentity_(ctx, username) -} -func (j *JIMM) CountIdentities(ctx context.Context, user *openfga.User) (int, error) { - if j.CountIdentities_ == nil { - return 0, errors.E(errors.CodeNotImplemented) - } - return j.CountIdentities_(ctx, user) -} -func (j *JIMM) ListIdentities(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) { - if j.ListIdentities_ == nil { - return nil, errors.E(errors.CodeNotImplemented) - } - return j.ListIdentities_(ctx, user, pagination, match) -} func (j *JIMM) GetUserCloudAccess(ctx context.Context, user *openfga.User, cloud names.CloudTag) (string, error) { if j.GetUserCloudAccess_ == nil { return "", errors.E(errors.CodeNotImplemented) @@ -391,12 +377,6 @@ func (j *JIMM) RevokeOfferAccess(ctx context.Context, user *openfga.User, offerU } return j.RevokeOfferAccess_(ctx, user, offerURL, ut, access) } -func (j *JIMM) SetIdentityModelDefaults(ctx context.Context, user *dbmodel.Identity, configs map[string]interface{}) error { - if j.SetIdentityModelDefaults_ == nil { - return errors.E(errors.CodeNotImplemented) - } - return j.SetIdentityModelDefaults_(ctx, user, configs) -} func (j *JIMM) ToJAASTag(ctx context.Context, tag *ofganames.Tag, resolveUUIDs bool) (string, error) { if j.ToJAASTag_ == nil { return "", errors.E(errors.CodeNotImplemented) diff --git a/internal/testutils/jimmtest/mocks/jimm_controller_mock.go b/internal/testutils/jimmtest/mocks/jimm_controller_mock.go index 501b5599f..57bf530ac 100644 --- a/internal/testutils/jimmtest/mocks/jimm_controller_mock.go +++ b/internal/testutils/jimmtest/mocks/jimm_controller_mock.go @@ -1,4 +1,5 @@ // Copyright 2024 Canonical. + package mocks import ( diff --git a/internal/testutils/jimmtest/mocks/jimm_identity_mock.go b/internal/testutils/jimmtest/mocks/jimm_identity_mock.go new file mode 100644 index 000000000..6a1d18110 --- /dev/null +++ b/internal/testutils/jimmtest/mocks/jimm_identity_mock.go @@ -0,0 +1,37 @@ +// Copyright 2024 Canonical. + +package mocks + +import ( + "context" + + "github.com/canonical/jimm/v3/internal/common/pagination" + "github.com/canonical/jimm/v3/internal/errors" + "github.com/canonical/jimm/v3/internal/openfga" +) + +// IdentityManager is an implementation of the jimm.IdentityManager interface. +type IdentityManager struct { + FetchIdentity_ func(ctx context.Context, id string) (*openfga.User, error) + ListIdentities_ func(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) + CountIdentities_ func(ctx context.Context, user *openfga.User) (int, error) +} + +func (i *IdentityManager) FetchIdentity(ctx context.Context, id string) (*openfga.User, error) { + if i.FetchIdentity_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return i.FetchIdentity_(ctx, id) +} +func (i *IdentityManager) ListIdentities(ctx context.Context, user *openfga.User, pagination pagination.LimitOffsetPagination, match string) ([]openfga.User, error) { + if i.ListIdentities_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return i.ListIdentities_(ctx, user, pagination, match) +} +func (i *IdentityManager) CountIdentities(ctx context.Context, user *openfga.User) (int, error) { + if i.CountIdentities_ == nil { + return 0, errors.E(errors.CodeNotImplemented) + } + return i.CountIdentities_(ctx, user) +}