roles
This commit is contained in:
parent
6319da650c
commit
3fdc8cb489
26 changed files with 2245 additions and 488 deletions
242
internal/store/roles_integration_test.go
Normal file
242
internal/store/roles_integration_test.go
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/danyel/go-loose/internal/role"
|
||||
"github.com/danyel/go-loose/internal/store"
|
||||
)
|
||||
|
||||
// rolesFixture creates a tenant owned by owner plus a developer member. The
|
||||
// developer is the interesting actor: it administers members but does not hold
|
||||
// roles.manage, so it is the boundary the escalation guards are tested against.
|
||||
func rolesFixture(t *testing.T) (s *store.Store, ctx context.Context, owner, developer, tenantID string) {
|
||||
t.Helper()
|
||||
s, _, ctx = integrationStore(t)
|
||||
owner = newUser(t, s, ctx, "owner@example.test")
|
||||
tenantID = newTenant(t, s, ctx, owner, "nmbs").ID
|
||||
developer = addMember(t, s, ctx, owner, tenantID, "developer@example.test", "developer")
|
||||
return s, ctx, owner, developer, tenantID
|
||||
}
|
||||
|
||||
func systemRole(t *testing.T, s *store.Store, ctx context.Context, slug string) string {
|
||||
t.Helper()
|
||||
id, err := s.SystemRoleID(ctx, slug)
|
||||
if err != nil {
|
||||
t.Fatalf("look up built-in role %q: %v", slug, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func TestTenantCanDefineItsOwnRoleAndItIsEnforced(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
application, err := s.CreateApplication(ctx, owner, store.Application{
|
||||
TenantID: tenantID, Slug: "guess", Name: "Guess", AllowedHosts: []string{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create application: %v", err)
|
||||
}
|
||||
|
||||
created, err := s.CreateRole(ctx, owner, tenantID, "release-manager",
|
||||
"Release manager", "Prepares releases.",
|
||||
[]string{string(role.ReadConsole), string(role.ManageContracts), string(role.ReadUsers)})
|
||||
if err != nil {
|
||||
t.Fatalf("create role: %v", err)
|
||||
}
|
||||
if created.System {
|
||||
t.Error("a tenant role must not be marked as built in")
|
||||
}
|
||||
if created.TenantID == nil || *created.TenantID != tenantID {
|
||||
t.Errorf("tenant id = %v, want %s", created.TenantID, tenantID)
|
||||
}
|
||||
if !contains(created.Permissions, string(role.ManageContracts)) {
|
||||
t.Errorf("permissions = %v, want contracts.manage", created.Permissions)
|
||||
}
|
||||
|
||||
// The role grants exactly what it says and nothing more.
|
||||
member := addMember(t, s, ctx, owner, tenantID, "member@example.test", "viewer")
|
||||
if err := s.SetUserAccess(ctx, owner, tenantID, member, created.ID, nil); err != nil {
|
||||
t.Fatalf("assign the custom role: %v", err)
|
||||
}
|
||||
_, err = s.CreateAPIKey(ctx, member, application.ID, "key", "gl_prefix", secretHash("custom-denied"), nil)
|
||||
assertPermission(t, "custom role issuing a key", err, false)
|
||||
_, err = s.SaveContract(ctx, member, application.ID, contractDocument(), nil)
|
||||
assertPermission(t, "custom role importing a contract", err, true)
|
||||
}
|
||||
|
||||
func TestCustomRoleIsScopedToItsOwnTenant(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
other := newUser(t, s, ctx, "other-owner@example.test")
|
||||
otherTenant := newTenant(t, s, ctx, other, "ypto").ID
|
||||
|
||||
created, err := s.CreateRole(ctx, owner, tenantID, "auditor", "Auditor", "", []string{string(role.ReadConsole)})
|
||||
if err != nil {
|
||||
t.Fatalf("create role: %v", err)
|
||||
}
|
||||
|
||||
roles, err := s.ListRoles(ctx, other, otherTenant)
|
||||
if err != nil {
|
||||
t.Fatalf("list roles: %v", err)
|
||||
}
|
||||
for _, item := range roles {
|
||||
if item.ID == created.ID {
|
||||
t.Fatal("a role leaked across tenants")
|
||||
}
|
||||
}
|
||||
if err := s.DeleteRole(ctx, other, created.ID); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("cross-tenant delete = %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := s.UpdateRole(ctx, other, created.ID, "Hijacked", "", nil); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("cross-tenant update = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInRolesCannotBeEditedOrDeleted(t *testing.T) {
|
||||
s, ctx, owner, _, _ := rolesFixture(t)
|
||||
ownerRole := systemRole(t, s, ctx, "owner")
|
||||
if _, err := s.UpdateRole(ctx, owner, ownerRole, "Supreme", "", nil); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("editing a built-in role = %v, want ErrNotFound", err)
|
||||
}
|
||||
if err := s.DeleteRole(ctx, owner, ownerRole); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("deleting a built-in role = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleInUseCannotBeDeleted(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
created, err := s.CreateRole(ctx, owner, tenantID, "support", "Support", "", []string{string(role.ReadConsole)})
|
||||
if err != nil {
|
||||
t.Fatalf("create role: %v", err)
|
||||
}
|
||||
member := addMember(t, s, ctx, owner, tenantID, "member@example.test", "viewer")
|
||||
if err := s.SetUserAccess(ctx, owner, tenantID, member, created.ID, nil); err != nil {
|
||||
t.Fatalf("assign role: %v", err)
|
||||
}
|
||||
if err := s.DeleteRole(ctx, owner, created.ID); !errors.Is(err, store.ErrRoleInUse) {
|
||||
t.Fatalf("delete an assigned role = %v, want ErrRoleInUse", err)
|
||||
}
|
||||
// Once nobody holds it the delete goes through.
|
||||
if err := s.SetUserAccess(ctx, owner, tenantID, member, systemRole(t, s, ctx, "viewer"), nil); err != nil {
|
||||
t.Fatalf("move the member off the role: %v", err)
|
||||
}
|
||||
if err := s.DeleteRole(ctx, owner, created.ID); err != nil {
|
||||
t.Fatalf("delete an unused role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRoleAssignmentCannotEscalate is the guard that matters most now that
|
||||
// developer and operator may manage members: they must not be able to hand
|
||||
// somebody a role more powerful than their own, which would otherwise be a
|
||||
// straightforward route to owner.
|
||||
func TestRoleAssignmentCannotEscalate(t *testing.T) {
|
||||
s, ctx, owner, developer, tenantID := rolesFixture(t)
|
||||
victim := addMember(t, s, ctx, owner, tenantID, "victim@example.test", "viewer")
|
||||
ownerRole := systemRole(t, s, ctx, "owner")
|
||||
|
||||
if err := s.SetUserAccess(ctx, developer, tenantID, victim, ownerRole, nil); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("developer promoting a member to owner = %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := s.InviteUser(ctx, developer, tenantID, "sneaky@example.test", "Sneaky", ownerRole, "hash"); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("developer inviting as owner = %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := s.CreateRole(ctx, developer, tenantID, "shadow-owner", "Shadow", "",
|
||||
[]string{string(role.ReadConsole), string(role.ManageTenants)}); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("developer creating an owner-grade role = %v, want ErrNotFound", err)
|
||||
}
|
||||
// Nor may they define any role at all: roles.manage is not theirs to hold,
|
||||
// however weak the role they were trying to mint.
|
||||
if _, err := s.CreateRole(ctx, developer, tenantID, "teammate", "Teammate", "",
|
||||
[]string{string(role.ReadConsole), string(role.ReadUsers)}); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("developer creating any role = %v, want ErrNotFound", err)
|
||||
}
|
||||
// The developer can still maintain members using the roles already offered.
|
||||
if _, err := s.InviteUser(ctx, developer, tenantID, "teammate@example.test", "Teammate",
|
||||
systemRole(t, s, ctx, "viewer"), "hash"); err != nil {
|
||||
t.Fatalf("developer inviting a member: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRolesManageIsRequiredToDefineRoles(t *testing.T) {
|
||||
s, ctx, _, developer, tenantID := rolesFixture(t)
|
||||
if _, err := s.ListRoles(ctx, developer, tenantID); err != nil {
|
||||
t.Fatalf("a developer may read the role catalog: %v", err)
|
||||
}
|
||||
if _, err := s.CreateRole(ctx, developer, tenantID, "nope", "Nope", "",
|
||||
[]string{string(role.ReadConsole)}); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Fatalf("developer creating a role = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignableRolesExcludeAnythingAboveTheActor(t *testing.T) {
|
||||
s, ctx, _, developer, tenantID := rolesFixture(t)
|
||||
assignable, err := s.AssignableRoles(ctx, developer, tenantID)
|
||||
if err != nil {
|
||||
t.Fatalf("assignable roles: %v", err)
|
||||
}
|
||||
ownerRole := systemRole(t, s, ctx, "owner")
|
||||
found := 0
|
||||
for _, item := range assignable {
|
||||
if !item.Assignable {
|
||||
t.Errorf("role %q was returned but is not marked assignable", item.Slug)
|
||||
}
|
||||
if contains(item.Permissions, string(role.ManageTenants)) {
|
||||
t.Errorf("developer may assign %q, which grants tenants.manage", item.Slug)
|
||||
}
|
||||
if item.ID == ownerRole {
|
||||
t.Error("a developer must not be offered the owner role")
|
||||
}
|
||||
found++
|
||||
}
|
||||
if found == 0 {
|
||||
t.Fatal("a developer should still be offered the roles within their authority")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownPermissionIsRejectedWhenDefiningARole(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
_, err := s.CreateRole(ctx, owner, tenantID, "sneaky", "Sneaky", "",
|
||||
[]string{string(role.ReadConsole), "superpower"})
|
||||
if err == nil {
|
||||
t.Fatal("a role must not be creatable with an unknown permission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDuplicateRoleSlugIsRejected(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
if _, err := s.CreateRole(ctx, owner, tenantID, "auditor", "Auditor", "",
|
||||
[]string{string(role.ReadConsole)}); err != nil {
|
||||
t.Fatalf("create role: %v", err)
|
||||
}
|
||||
_, err := s.CreateRole(ctx, owner, tenantID, "auditor", "Another", "",
|
||||
[]string{string(role.ReadConsole)})
|
||||
if !errors.Is(err, store.ErrRoleSlugTaken) {
|
||||
t.Fatalf("duplicate slug = %v, want ErrRoleSlugTaken", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTenantKeepsAnOwner guards the demotion path: the last owner of a tenant
|
||||
// must not be able to walk away from it and lock every administrator out.
|
||||
func TestTenantKeepsAnOwner(t *testing.T) {
|
||||
s, ctx, owner, _, tenantID := rolesFixture(t)
|
||||
admin := addMember(t, s, ctx, owner, tenantID, "admin@example.test", "admin")
|
||||
adminRole := systemRole(t, s, ctx, "admin")
|
||||
ownerRole := systemRole(t, s, ctx, "owner")
|
||||
|
||||
// The owner cannot simply step down while nobody else holds the role.
|
||||
if err := s.SetUserAccess(ctx, owner, tenantID, owner, adminRole, nil); !errors.Is(err, store.ErrLastOwner) {
|
||||
t.Fatalf("owner stepping down alone = %v, want ErrLastOwner", err)
|
||||
}
|
||||
// Handing the role over first makes the step down legal.
|
||||
if err := s.SetUserAccess(ctx, owner, tenantID, admin, ownerRole, nil); err != nil {
|
||||
t.Fatalf("promote the admin to owner: %v", err)
|
||||
}
|
||||
if err := s.SetUserAccess(ctx, admin, tenantID, owner, adminRole, nil); err != nil {
|
||||
t.Fatalf("step down once a second owner exists: %v", err)
|
||||
}
|
||||
// The admin is now the only owner and must not be able to walk away from it.
|
||||
if err := s.SetUserAccess(ctx, admin, tenantID, admin, adminRole, nil); !errors.Is(err, store.ErrLastOwner) {
|
||||
t.Fatalf("demoting the last owner = %v, want ErrLastOwner", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue