From 10238a4e4e45345ecbeeee70f865be2ce29fcf70 Mon Sep 17 00:00:00 2001 From: Mike Mason Date: Wed, 2 Oct 2024 13:29:55 -0500 Subject: [PATCH] add manager field to simplify multi service management (#292) This adds a manager field to roles and role bindings allowing services to mark the manager of a specific role / role binding. This allows the managing service to easily filter out unmanaged roles / role bindings which it does not own which allows for cleaner logic for reconciling roles/role bindings. Both roles and role binding create requests can provide an optional `manager` field and role / role binding list endpoints can now be provided an optional `manager` query value. Signed-off-by: Mike Mason --- cmd/createrole.go | 4 +- internal/api/rolebindings.go | 17 +++- internal/api/roles.go | 18 +++- internal/api/roles_v2.go | 22 ++++- internal/api/types.go | 9 +- internal/query/example_policy_test.go | 14 +-- internal/query/mock/mock.go | 21 +++- internal/query/relations.go | 82 +++++++++++++++- internal/query/relations_test.go | 15 +-- internal/query/rolebindings.go | 28 +++++- internal/query/rolebindings_test.go | 34 +++---- internal/query/roles_v2.go | 96 ++++++++++++++++++- internal/query/roles_v2_test.go | 20 ++-- internal/query/service.go | 12 ++- .../20241001000000_manager_fields.sql | 7 ++ internal/storage/rolebinding.go | 63 ++++++++++-- internal/storage/rolebinding_test.go | 10 +- internal/storage/roles.go | 74 ++++++++++++-- internal/storage/roles_test.go | 12 +-- internal/types/types.go | 2 + openapi-v2.yaml | 64 +++++++++---- 21 files changed, 509 insertions(+), 115 deletions(-) create mode 100644 internal/storage/migrations/20241001000000_manager_fields.sql diff --git a/cmd/createrole.go b/cmd/createrole.go index 9b998c2e..95b27e1b 100644 --- a/cmd/createrole.go +++ b/cmd/createrole.go @@ -113,7 +113,7 @@ func createRole(ctx context.Context, cfg *config.AppConfig) { logger.Fatalw("error creating subject resource", "error", err) } - role, err := engine.CreateRoleV2(ctx, subjectResource, resource, name, actions) + role, err := engine.CreateRoleV2(ctx, subjectResource, resource, "", name, actions) if err != nil { logger.Fatalw("error creating role", "error", err) } @@ -125,7 +125,7 @@ func createRole(ctx context.Context, cfg *config.AppConfig) { logger.Fatalw("error creating role resource", "error", err) } - rb, err := engine.CreateRoleBinding(ctx, subjectResource, resource, roleres, rbsubj) + rb, err := engine.CreateRoleBinding(ctx, subjectResource, resource, roleres, "", rbsubj) if err != nil { logger.Fatalw("error creating role binding", "error", err) } diff --git a/internal/api/rolebindings.go b/internal/api/rolebindings.go index ecde2620..e411744a 100644 --- a/internal/api/rolebindings.go +++ b/internal/api/rolebindings.go @@ -73,7 +73,7 @@ func (r *Router) roleBindingCreate(c echo.Context) error { } } - rb, err := r.engine.CreateRoleBinding(ctx, actor, resource, roleResource, subjects) + rb, err := r.engine.CreateRoleBinding(ctx, actor, resource, roleResource, body.Manager, subjects) if err != nil { return r.errorResponse("error creating role-binding", err) } @@ -83,6 +83,7 @@ func (r *Router) roleBindingCreate(c echo.Context) error { roleBindingResponse{ ID: rb.ID, ResourceID: rb.ResourceID, + Manager: rb.Manager, SubjectIDs: rb.SubjectIDs, RoleID: rb.RoleID, @@ -122,7 +123,16 @@ func (r *Router) roleBindingsList(c echo.Context) error { return err } - rbs, err := r.engine.ListRoleBindings(ctx, resource, nil) + var rbs []types.RoleBinding + + params := c.QueryParams() + + if params.Has("manager") { + rbs, err = r.engine.ListManagerRoleBindings(ctx, params.Get("manager"), resource, nil) + } else { + rbs, err = r.engine.ListRoleBindings(ctx, resource, nil) + } + if err != nil { return r.errorResponse("error listing role-binding", err) } @@ -137,6 +147,7 @@ func (r *Router) roleBindingsList(c echo.Context) error { ResourceID: rb.ResourceID, SubjectIDs: rb.SubjectIDs, RoleID: rb.RoleID, + Manager: rb.Manager, CreatedBy: rb.CreatedBy, UpdatedBy: rb.UpdatedBy, @@ -242,6 +253,7 @@ func (r *Router) roleBindingGet(c echo.Context) error { ResourceID: rb.ResourceID, SubjectIDs: rb.SubjectIDs, RoleID: rb.RoleID, + Manager: rb.Manager, CreatedBy: rb.CreatedBy, UpdatedBy: rb.UpdatedBy, @@ -321,6 +333,7 @@ func (r *Router) roleBindingUpdate(c echo.Context) error { ResourceID: rb.ResourceID, SubjectIDs: rb.SubjectIDs, RoleID: rb.RoleID, + Manager: rb.Manager, CreatedBy: rb.CreatedBy, UpdatedBy: rb.UpdatedBy, diff --git a/internal/api/roles.go b/internal/api/roles.go index 2e9895e3..b9714ffa 100644 --- a/internal/api/roles.go +++ b/internal/api/roles.go @@ -14,6 +14,7 @@ import ( "go.infratographer.com/permissions-api/internal/iapl" "go.infratographer.com/permissions-api/internal/query" "go.infratographer.com/permissions-api/internal/storage" + "go.infratographer.com/permissions-api/internal/types" ) func (r *Router) roleCreate(c echo.Context) error { @@ -49,7 +50,7 @@ func (r *Router) roleCreate(c echo.Context) error { } role, err := r.engine.CreateRole( - ctx, subjectResource, resource, + ctx, subjectResource, resource, reqBody.Manager, strings.TrimSpace(reqBody.Name), reqBody.Actions, ) @@ -66,6 +67,7 @@ func (r *Router) roleCreate(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -139,6 +141,7 @@ func (r *Router) roleUpdate(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -202,6 +205,7 @@ func (r *Router) roleGet(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -238,7 +242,16 @@ func (r *Router) rolesList(c echo.Context) error { return err } - roles, err := r.engine.ListRoles(ctx, resource) + var roles []types.Role + + params := c.QueryParams() + + if params.Has("manager") { + roles, err = r.engine.ListManagerRoles(ctx, params.Get("manager"), resource) + } else { + roles, err = r.engine.ListRoles(ctx, resource) + } + if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, "error getting role").SetInternal(err) } @@ -251,6 +264,7 @@ func (r *Router) rolesList(c echo.Context) error { roleResp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, CreatedBy: role.CreatedBy, UpdatedBy: role.UpdatedBy, diff --git a/internal/api/roles_v2.go b/internal/api/roles_v2.go index 05151892..5e1f05ee 100644 --- a/internal/api/roles_v2.go +++ b/internal/api/roles_v2.go @@ -7,6 +7,7 @@ import ( "time" "go.infratographer.com/permissions-api/internal/iapl" + "go.infratographer.com/permissions-api/internal/types" "github.com/labstack/echo/v4" "go.infratographer.com/x/gidx" @@ -47,7 +48,7 @@ func (r *Router) roleV2Create(c echo.Context) error { } role, err := r.engine.CreateRoleV2( - ctx, subjectResource, resource, + ctx, subjectResource, resource, reqBody.Manager, strings.TrimSpace(reqBody.Name), reqBody.Actions, ) if err != nil { @@ -57,6 +58,7 @@ func (r *Router) roleV2Create(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -113,6 +115,7 @@ func (r *Router) roleV2Update(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -159,6 +162,7 @@ func (r *Router) roleV2Get(c echo.Context) error { resp := roleResponse{ ID: role.ID, Name: role.Name, + Manager: role.Manager, Actions: role.Actions, ResourceID: role.ResourceID, CreatedBy: role.CreatedBy, @@ -195,7 +199,16 @@ func (r *Router) roleV2sList(c echo.Context) error { return err } - roles, err := r.engine.ListRolesV2(ctx, resource) + var roles []types.Role + + params := c.QueryParams() + + if params.Has("manager") { + roles, err = r.engine.ListManagerRolesV2(ctx, params.Get("manager"), resource) + } else { + roles, err = r.engine.ListRolesV2(ctx, resource) + } + if err != nil { return r.errorResponse("error getting roles", err) } @@ -206,8 +219,9 @@ func (r *Router) roleV2sList(c echo.Context) error { for _, role := range roles { roleResp := listRolesV2Role{ - ID: role.ID, - Name: role.Name, + ID: role.ID, + Name: role.Name, + Manager: role.Manager, } resp.Data = append(resp.Data, roleResp) diff --git a/internal/api/types.go b/internal/api/types.go index 90330e43..dd44dc46 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -6,6 +6,7 @@ import ( type createRoleRequest struct { Name string `json:"name" binding:"required"` + Manager string `json:"manager"` Actions []string `json:"actions" binding:"required"` } @@ -17,6 +18,7 @@ type updateRoleRequest struct { type roleResponse struct { ID gidx.PrefixedID `json:"id"` Name string `json:"name"` + Manager string `json:"manager,omitempty"` Actions []string `json:"actions"` ResourceID gidx.PrefixedID `json:"resource_id,omitempty"` @@ -77,8 +79,9 @@ type listRolesV2Response struct { } type listRolesV2Role struct { - ID gidx.PrefixedID `json:"id"` - Name string `json:"name"` + ID gidx.PrefixedID `json:"id"` + Name string `json:"name"` + Manager string `json:"manager"` } // RoleBindings @@ -86,6 +89,7 @@ type listRolesV2Role struct { type roleBindingRequest struct { RoleID string `json:"role_id" binding:"required"` SubjectIDs []gidx.PrefixedID `json:"subject_ids" binding:"required"` + Manager string `json:"manager"` } type rolebindingUpdateRequest struct { @@ -96,6 +100,7 @@ type roleBindingResponse struct { ID gidx.PrefixedID `json:"id"` ResourceID gidx.PrefixedID `json:"resource_id"` RoleID gidx.PrefixedID `json:"role_id"` + Manager string `json:"manager"` SubjectIDs []gidx.PrefixedID `json:"subject_ids"` CreatedBy gidx.PrefixedID `json:"created_by"` diff --git a/internal/query/example_policy_test.go b/internal/query/example_policy_test.go index 3446c611..7364bfa2 100644 --- a/internal/query/example_policy_test.go +++ b/internal/query/example_policy_test.go @@ -179,13 +179,13 @@ func TestExamplePolicy(t *testing.T) { require.NoError(t, err) // create roles - superadmin, err := e.CreateRoleV2(ctx, superuser, tnnttenroot, "superuser", allactions) + superadmin, err := e.CreateRoleV2(ctx, superuser, tnnttenroot, t.Name(), "superuser", allactions) require.NoError(t, err) - iamadmin, err := e.CreateRoleV2(ctx, superuser, tnnttena, "iam_admin", iamactions) + iamadmin, err := e.CreateRoleV2(ctx, superuser, tnnttena, t.Name(), "iam_admin", iamactions) require.NoError(t, err) - lbadmin, err := e.CreateRoleV2(ctx, superuser, tnnttenroot, "lb_admin", lbactions) + lbadmin, err := e.CreateRoleV2(ctx, superuser, tnnttenroot, t.Name(), "lb_admin", lbactions) require.NoError(t, err) tc := []testingx.TestCase[any, any]{ @@ -193,7 +193,7 @@ func TestExamplePolicy(t *testing.T) { Name: "superuser can do anything", SetupFn: func(ctx context.Context, t *testing.T) context.Context { role := types.Resource{Type: "role", ID: superadmin.ID} - _, err := e.CreateRoleBinding(ctx, superuser, tnnttenroot, role, []types.RoleBindingSubject{{SubjectResource: superuser}}) + _, err := e.CreateRoleBinding(ctx, superuser, tnnttenroot, role, t.Name(), []types.RoleBindingSubject{{SubjectResource: superuser}}) require.NoError(t, err) return ctx @@ -229,7 +229,7 @@ func TestExamplePolicy(t *testing.T) { Sync: true, SetupFn: func(ctx context.Context, t *testing.T) context.Context { role := types.Resource{Type: "role", ID: lbadmin.ID} - _, err := e.CreateRoleBinding(ctx, superuser, tnnttena, role, []types.RoleBindingSubject{{SubjectResource: groupadmin}}) + _, err := e.CreateRoleBinding(ctx, superuser, tnnttena, role, t.Name(), []types.RoleBindingSubject{{SubjectResource: groupadmin}}) require.NoError(t, err) return ctx @@ -279,7 +279,7 @@ func TestExamplePolicy(t *testing.T) { Sync: true, SetupFn: func(ctx context.Context, t *testing.T) context.Context { role := types.Resource{Type: "role", ID: iamadmin.ID} - _, err := e.CreateRoleBinding(ctx, superuser, tnnttena, role, []types.RoleBindingSubject{{SubjectResource: groupadminsubgroup}}) + _, err := e.CreateRoleBinding(ctx, superuser, tnnttena, role, t.Name(), []types.RoleBindingSubject{{SubjectResource: groupadminsubgroup}}) require.NoError(t, err) return ctx @@ -326,7 +326,7 @@ func TestExamplePolicy(t *testing.T) { Name: "iam-admin cannot be bind on tnntten-root", CheckFn: func(ctx context.Context, t *testing.T, tr testingx.TestResult[any]) { role := types.Resource{Type: "role", ID: iamadmin.ID} - _, err := e.CreateRoleBinding(ctx, superuser, tnnttenroot, role, []types.RoleBindingSubject{{SubjectResource: groupadminsubgroup}}) + _, err := e.CreateRoleBinding(ctx, superuser, tnnttenroot, role, t.Name(), []types.RoleBindingSubject{{SubjectResource: groupadminsubgroup}}) assert.Error(t, err) assert.ErrorIs(t, err, ErrRoleNotFound) }, diff --git a/internal/query/mock/mock.go b/internal/query/mock/mock.go index 1610ffc7..75744aed 100644 --- a/internal/query/mock/mock.go +++ b/internal/query/mock/mock.go @@ -50,7 +50,7 @@ func (e *Engine) CreateRelationships(context.Context, []types.Relationship) erro } // CreateRole creates a Role object and does not persist it anywhere. -func (e *Engine) CreateRole(context.Context, types.Resource, types.Resource, string, []string) (types.Role, error) { +func (e *Engine) CreateRole(context.Context, types.Resource, types.Resource, string, string, []string) (types.Role, error) { args := e.Called() retRole := args.Get(0).(types.Role) @@ -60,7 +60,7 @@ func (e *Engine) CreateRole(context.Context, types.Resource, types.Resource, str // CreateRoleV2 creates a v2 role object // TODO: Implement this -func (e *Engine) CreateRoleV2(context.Context, types.Resource, types.Resource, string, []string) (types.Role, error) { +func (e *Engine) CreateRoleV2(context.Context, types.Resource, types.Resource, string, string, []string) (types.Role, error) { return types.Role{}, nil } @@ -69,6 +69,11 @@ func (e *Engine) ListRolesV2(context.Context, types.Resource) ([]types.Role, err return nil, nil } +// ListManagerRolesV2 list roles +func (e *Engine) ListManagerRolesV2(context.Context, string, types.Resource) ([]types.Role, error) { + return nil, nil +} + // UpdateRole returns the provided mock results. func (e *Engine) UpdateRole(context.Context, types.Resource, types.Resource, string, []string) (types.Role, error) { args := e.Called() @@ -130,6 +135,11 @@ func (e *Engine) ListRoles(context.Context, types.Resource) ([]types.Role, error return nil, nil } +// ListManagerRoles returns nothing but satisfies the Engine interface. +func (e *Engine) ListManagerRoles(context.Context, string, types.Resource) ([]types.Role, error) { + return nil, nil +} + // DeleteRelationships does nothing but satisfies the Engine interface. func (e *Engine) DeleteRelationships(context.Context, ...types.Relationship) error { args := e.Called() @@ -209,7 +219,7 @@ func (e *Engine) SubjectHasPermission(context.Context, types.Resource, string, t } // CreateRoleBinding returns nothing but satisfies the Engine interface. -func (e *Engine) CreateRoleBinding(context.Context, types.Resource, types.Resource, types.Resource, []types.RoleBindingSubject) (types.RoleBinding, error) { +func (e *Engine) CreateRoleBinding(context.Context, types.Resource, types.Resource, types.Resource, string, []types.RoleBindingSubject) (types.RoleBinding, error) { return types.RoleBinding{}, nil } @@ -218,6 +228,11 @@ func (e *Engine) ListRoleBindings(context.Context, types.Resource, *types.Resour return nil, nil } +// ListManagerRoleBindings returns nothing but satisfies the Engine interface. +func (e *Engine) ListManagerRoleBindings(context.Context, string, types.Resource, *types.Resource) ([]types.RoleBinding, error) { + return nil, nil +} + // GetRoleBinding returns nothing but satisfies the Engine interface. func (e *Engine) GetRoleBinding(context.Context, types.Resource) (types.RoleBinding, error) { return types.RoleBinding{}, nil diff --git a/internal/query/relations.go b/internal/query/relations.go index be387a2a..84e1635f 100644 --- a/internal/query/relations.go +++ b/internal/query/relations.go @@ -304,7 +304,7 @@ func (e *engine) CreateRelationships(ctx context.Context, rels []types.Relations } // CreateRole creates a role scoped to the given resource with the given actions. -func (e *engine) CreateRole(ctx context.Context, actor, res types.Resource, roleName string, actions []string) (types.Role, error) { +func (e *engine) CreateRole(ctx context.Context, actor, res types.Resource, manager, roleName string, actions []string) (types.Role, error) { ctx, span := e.tracer.Start(ctx, "engine.CreateRole") defer span.End() @@ -321,7 +321,7 @@ func (e *engine) CreateRole(ctx context.Context, actor, res types.Resource, role return types.Role{}, nil } - dbRole, err := e.store.CreateRole(dbCtx, actor.ID, role.ID, roleName, res.ID) + dbRole, err := e.store.CreateRole(dbCtx, actor.ID, role.ID, roleName, manager, res.ID) if err != nil { return types.Role{}, err } @@ -351,6 +351,7 @@ func (e *engine) CreateRole(ctx context.Context, actor, res types.Resource, role return types.Role{}, err } + role.Manager = dbRole.Manager role.CreatedBy = dbRole.CreatedBy role.UpdatedBy = dbRole.UpdatedBy role.ResourceID = dbRole.ResourceID @@ -497,6 +498,7 @@ func (e *engine) UpdateRole(ctx context.Context, actor, roleResource types.Resou } role.Name = dbRole.Name + role.Manager = dbRole.Manager role.CreatedBy = dbRole.CreatedBy role.UpdatedBy = dbRole.UpdatedBy role.ResourceID = dbRole.ResourceID @@ -899,6 +901,81 @@ func (e *engine) ListRoles(ctx context.Context, resource types.Resource) ([]type out[i] = types.Role{ ID: dbRole.ID, Name: dbRole.Name, + Manager: dbRole.Manager, + Actions: spicedbRole.Actions, + ResourceID: dbRole.ResourceID, + CreatedBy: dbRole.CreatedBy, + UpdatedBy: dbRole.UpdatedBy, + CreatedAt: dbRole.CreatedAt, + UpdatedAt: dbRole.UpdatedAt, + } + } + + return out, nil +} + +// ListManagerRoles returns all roles bound to a given resource. +func (e *engine) ListManagerRoles(ctx context.Context, manager string, resource types.Resource) ([]types.Role, error) { + dbRoles, err := e.store.ListResourceRoles(ctx, resource.ID) + if err != nil { + return nil, err + } + + dbRolesv1 := make([]storage.Role, 0, len(dbRoles)) + + for _, dbRole := range dbRoles { + if dbRole.Manager != manager { + continue + } + + res, err := e.NewResourceFromID(dbRole.ID) + if err != nil { + return nil, err + } + + if res.Type == e.rbac.RoleResource.Name { + continue + } + + dbRolesv1 = append(dbRolesv1, dbRole) + } + + resType := e.namespace + "/" + resource.Type + roleType := e.namespace + "/role" + + filter := &pb.RelationshipFilter{ + ResourceType: resType, + OptionalResourceId: resource.ID.String(), + OptionalSubjectFilter: &pb.SubjectFilter{ + SubjectType: roleType, + OptionalRelation: &pb.SubjectFilter_RelationFilter{ + Relation: roleSubjectRelation, + }, + }, + } + + relationships, err := e.readRelationships(ctx, filter) + if err != nil { + return nil, err + } + + spicedbRoles := relationshipsToRoles(relationships) + + rolesByID := make(map[gidx.PrefixedID]types.Role, len(spicedbRoles)) + + for _, role := range spicedbRoles { + rolesByID[role.ID] = role + } + + out := make([]types.Role, len(dbRolesv1)) + + for i, dbRole := range dbRolesv1 { + spicedbRole := rolesByID[dbRole.ID] + + out[i] = types.Role{ + ID: dbRole.ID, + Name: dbRole.Name, + Manager: dbRole.Manager, Actions: spicedbRole.Actions, ResourceID: dbRole.ResourceID, CreatedBy: dbRole.CreatedBy, @@ -965,6 +1042,7 @@ func (e *engine) GetRole(ctx context.Context, roleResource types.Resource) (type out := types.Role{ ID: roleResource.ID, Name: dbRole.Name, + Manager: dbRole.Manager, Actions: actions, ResourceID: dbRole.ResourceID, diff --git a/internal/query/relations_test.go b/internal/query/relations_test.go index c74aef5d..65a0a49c 100644 --- a/internal/query/relations_test.go +++ b/internal/query/relations_test.go @@ -143,7 +143,7 @@ func TestCreateRoles(t *testing.T) { actorRes, err := e.NewResourceFromID(gidx.MustNewID("idntusr")) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, "test", actions) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), "test", actions) if err != nil { return testingx.TestResult[types.Role]{ Err: err, @@ -175,7 +175,7 @@ func TestGetRoles(t *testing.T) { actorRes, err := e.NewResourceFromID(gidx.MustNewID("idntusr")) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, "test", []string{"loadbalancer_get"}) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), "test", []string{"loadbalancer_get"}) require.NoError(t, err) roleRes, err := e.NewResourceFromID(role.ID) require.NoError(t, err) @@ -233,7 +233,7 @@ func TestRoleUpdate(t *testing.T) { actorUpdateRes, err := e.NewResourceFromID(gidx.MustNewID("idntusr")) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, "test", []string{"loadbalancer_get"}) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), "test", []string{"loadbalancer_get"}) require.NoError(t, err) roles, err := e.ListRoles(ctx, tenRes) require.NoError(t, err) @@ -317,7 +317,7 @@ func TestListRoles(t *testing.T) { tenRes, err := e.NewResourceFromID(tenID) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), []string{"loadbalancer_get"}) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), t.Name(), []string{"loadbalancer_get"}) require.NoError(t, err) require.NotEmpty(t, role.ID) @@ -342,7 +342,7 @@ func TestListRoles(t *testing.T) { tenRes, err := e.NewResourceFromID(tenID) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), nil) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), t.Name(), nil) require.NoError(t, err) require.NotEmpty(t, role.ID) @@ -386,7 +386,7 @@ func TestRoleDelete(t *testing.T) { actorRes, err := e.NewResourceFromID(gidx.MustNewID("idntusr")) require.NoError(t, err) - role, err := e.CreateRole(ctx, actorRes, tenRes, "test", []string{"loadbalancer_get"}) + role, err := e.CreateRole(ctx, actorRes, tenRes, t.Name(), "test", []string{"loadbalancer_get"}) require.NoError(t, err) roles, err := e.ListRoles(ctx, tenRes) require.NoError(t, err) @@ -455,6 +455,7 @@ func TestAssignments(t *testing.T) { ctx, actorRes, tenRes, + t.Name(), "test", []string{ "loadbalancer_update", @@ -515,6 +516,7 @@ func TestUnassignments(t *testing.T) { ctx, actorRes, tenRes, + t.Name(), "test", []string{ "loadbalancer_update", @@ -768,6 +770,7 @@ func TestSubjectActions(t *testing.T) { ctx, actorRes, tenRes, + t.Name(), "test", []string{ "loadbalancer_update", diff --git a/internal/query/rolebindings.go b/internal/query/rolebindings.go index 7250bc41..28b696e5 100644 --- a/internal/query/rolebindings.go +++ b/internal/query/rolebindings.go @@ -92,6 +92,7 @@ func (e *engine) GetRoleBinding(ctx context.Context, roleBinding types.Resource) func (e *engine) CreateRoleBinding( ctx context.Context, actor, resource, roleResource types.Resource, + manager string, subjects []types.RoleBindingSubject, ) (types.RoleBinding, error) { ctx, span := e.tracer.Start( @@ -141,7 +142,7 @@ func (e *engine) CreateRoleBinding( return types.RoleBinding{}, err } - rb, err := e.store.CreateRoleBinding(dbCtx, actor.ID, rbid, resource.ID) + rb, err := e.store.CreateRoleBinding(dbCtx, actor.ID, rbid, resource.ID, manager) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -338,6 +339,27 @@ func (e *engine) ListRoleBindings(ctx context.Context, resource types.Resource, e.logger.Debugf("listing role-bindings for resource: %s, optionalRole: %v", resource.ID, optionalRole) + return e.listRoleBindings(ctx, resource, optionalRole, nil) +} + +func (e *engine) ListManagerRoleBindings(ctx context.Context, manager string, resource types.Resource, optionalRole *types.Resource) ([]types.RoleBinding, error) { + ctx, span := e.tracer.Start( + ctx, "engine.ListManagerRoleBinding", + trace.WithAttributes( + attribute.Stringer("resource_id", resource.ID), + attribute.String("manager", manager), + ), + ) + defer span.End() + + e.logger.Debugf("listing manager %s role-bindings for resource: %s, optionalRole: %v", manager, resource.ID, optionalRole) + + return e.listRoleBindings(ctx, resource, optionalRole, &manager) +} + +func (e *engine) listRoleBindings(ctx context.Context, resource types.Resource, optionalRole *types.Resource, optionalManager *string) ([]types.RoleBinding, error) { + span := trace.SpanFromContext(ctx) + // 1. list all grants on the resource listRbFilter := &pb.RelationshipFilter{ ResourceType: e.namespaced(resource.Type), @@ -388,6 +410,10 @@ func (e *engine) ListRoleBindings(ctx context.Context, resource types.Resource, continue } + if optionalManager != nil && rb.Manager != *optionalManager { + continue + } + if optionalRole != nil && rb.RoleID != optionalRole.ID { continue } diff --git a/internal/query/rolebindings_test.go b/internal/query/rolebindings_test.go index d9412db3..24836c02 100644 --- a/internal/query/rolebindings_test.go +++ b/internal/query/rolebindings_test.go @@ -47,7 +47,7 @@ func TestCreateRoleBinding(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - role, err := e.CreateRoleV2(ctx, subj, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + role, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) roleRes, err := e.NewResourceFromID(role.ID) @@ -145,7 +145,7 @@ func TestCreateRoleBinding(t *testing.T) { } testFn := func(ctx context.Context, in input) testingx.TestResult[types.RoleBinding] { - rb, err := e.CreateRoleBinding(ctx, actor, in.resource, in.role, in.subjects) + rb, err := e.CreateRoleBinding(ctx, actor, in.resource, in.role, t.Name(), in.subjects) return testingx.TestResult[types.RoleBinding]{Success: rb, Err: err} } @@ -166,10 +166,10 @@ func TestListRoleBindings(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - viewer, err := e.CreateRoleV2(ctx, subj, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + viewer, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) - editor, err := e.CreateRoleV2(ctx, subj, root, "lb_editor", []string{"loadbalancer_list", "loadbalancer_get", "loadbalancer_create", "loadbalancer_update"}) + editor, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_editor", []string{"loadbalancer_list", "loadbalancer_get", "loadbalancer_create", "loadbalancer_update"}) require.NoError(t, err) viewerRes, err := e.NewResourceFromID(viewer.ID) @@ -181,10 +181,10 @@ func TestListRoleBindings(t *testing.T) { notfoundRole, err := e.NewResourceFromIDString("permrv2-notfound") require.NoError(t, err) - _, err = e.CreateRoleBinding(ctx, actor, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + _, err = e.CreateRoleBinding(ctx, actor, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) - _, err = e.CreateRoleBinding(ctx, actor, root, editorRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + _, err = e.CreateRoleBinding(ctx, actor, root, editorRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) _, err = e.client.WriteRelationships(ctx, &pb.WriteRelationshipsRequest{ @@ -270,7 +270,7 @@ func TestGetRoleBinding(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - viewer, err := e.CreateRoleV2(ctx, subj, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + viewer, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) viewerRes, err := e.NewResourceFromID(viewer.ID) @@ -279,7 +279,7 @@ func TestGetRoleBinding(t *testing.T) { notfoundRB, err := e.NewResourceFromIDString("permrbn-notfound") require.NoError(t, err) - rb, err := e.CreateRoleBinding(ctx, actor, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + rb, err := e.CreateRoleBinding(ctx, actor, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) rbRes, err := e.NewResourceFromID(rb.ID) @@ -327,12 +327,12 @@ func TestUpdateRoleBinding(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - viewer, err := e.CreateRoleV2(ctx, subj, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + viewer, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) viewerRes, err := e.NewResourceFromID(viewer.ID) require.NoError(t, err) - rb, err := e.CreateRoleBinding(ctx, subj, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + rb, err := e.CreateRoleBinding(ctx, subj, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) rbRes, err := e.NewResourceFromID(rb.ID) require.NoError(t, err) @@ -425,12 +425,12 @@ func TestDeleteRoleBinding(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - viewer, err := e.CreateRoleV2(ctx, actor, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + viewer, err := e.CreateRoleV2(ctx, actor, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) viewerRes, err := e.NewResourceFromID(viewer.ID) require.NoError(t, err) - rb, err := e.CreateRoleBinding(ctx, actor, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: actor}}) + rb, err := e.CreateRoleBinding(ctx, actor, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: actor}}) require.NoError(t, err) rbRes, err := e.NewResourceFromID(rb.ID) require.NoError(t, err) @@ -492,7 +492,7 @@ func TestPermissions(t *testing.T) { require.NoError(t, err) // role - viewer, err := e.CreateRoleV2(ctx, actor, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + viewer, err := e.CreateRoleV2(ctx, actor, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) viewerRes, err := e.NewResourceFromID(viewer.ID) require.NoError(t, err) @@ -545,7 +545,7 @@ func TestPermissions(t *testing.T) { }) require.Error(t, err) - _, err = e.CreateRoleBinding(ctx, user1, lb1, viewerRes, []types.RoleBindingSubject{{SubjectResource: user1}}) + _, err = e.CreateRoleBinding(ctx, user1, lb1, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: user1}}) require.NoError(t, err) return ctx @@ -579,7 +579,7 @@ func TestPermissions(t *testing.T) { }) require.Error(t, err) - _, err = e.CreateRoleBinding(ctx, user1, child, viewerRes, []types.RoleBindingSubject{{SubjectResource: user1}}) + _, err = e.CreateRoleBinding(ctx, user1, child, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: user1}}) require.NoError(t, err) return ctx @@ -613,7 +613,7 @@ func TestPermissions(t *testing.T) { }) require.Error(t, err) - _, err = e.CreateRoleBinding(ctx, user1, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: user1}}) + _, err = e.CreateRoleBinding(ctx, user1, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: user1}}) require.NoError(t, err) return ctx @@ -647,7 +647,7 @@ func TestPermissions(t *testing.T) { }) require.Error(t, err) - rb, err = e.CreateRoleBinding(ctx, user1, root, viewerRes, []types.RoleBindingSubject{{SubjectResource: group1}}) + rb, err = e.CreateRoleBinding(ctx, user1, root, viewerRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: group1}}) require.NoError(t, err) return ctx diff --git a/internal/query/roles_v2.go b/internal/query/roles_v2.go index 5ef55924..a9f4dc74 100644 --- a/internal/query/roles_v2.go +++ b/internal/query/roles_v2.go @@ -22,7 +22,7 @@ func (e *engine) namespaced(name string) string { return e.namespace + "/" + name } -func (e *engine) CreateRoleV2(ctx context.Context, actor, owner types.Resource, roleName string, actions []string) (types.Role, error) { +func (e *engine) CreateRoleV2(ctx context.Context, actor, owner types.Resource, manager, roleName string, actions []string) (types.Role, error) { ctx, span := e.tracer.Start(ctx, "engine.CreateRoleV2") defer span.End() @@ -49,7 +49,7 @@ func (e *engine) CreateRoleV2(ctx context.Context, actor, owner types.Resource, return types.Role{}, nil } - dbRole, err := e.store.CreateRole(dbCtx, actor.ID, role.ID, roleName, owner.ID) + dbRole, err := e.store.CreateRole(dbCtx, actor.ID, role.ID, roleName, manager, owner.ID) if err != nil { span.RecordError(err) span.SetStatus(codes.Error, err.Error()) @@ -166,14 +166,100 @@ func (e *engine) ListRolesV2(ctx context.Context, owner types.Resource) ([]types for i, r := range storageRoles { roles[i] = types.Role{ - Name: r.Name, - ID: r.ID, + Name: r.Name, + ID: r.ID, + Manager: r.Manager, } } return roles, nil } +func (e *engine) ListManagerRolesV2(ctx context.Context, manager string, owner types.Resource) ([]types.Role, error) { + ctx, span := e.tracer.Start( + ctx, + "engine.ListManagerRolesV2", + trace.WithAttributes( + attribute.Stringer("owner", owner.ID), + attribute.String("manager", manager), + ), + ) + defer span.End() + + if _, ok := e.rbac.RoleOwnersSet()[owner.Type]; !ok { + err := fmt.Errorf("%w: %s is not a valid role owner", ErrInvalidType, owner.Type) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + + return nil, err + } + + lookupClient, err := e.client.LookupSubjects(ctx, &pb.LookupSubjectsRequest{ + Consistency: &pb.Consistency{ + Requirement: &pb.Consistency_FullyConsistent{ + FullyConsistent: true, + }, + }, + Resource: resourceToSpiceDBRef(e.namespace, owner), + Permission: iapl.AvailableRolesList, + SubjectObjectType: e.namespaced(e.rbac.RoleResource.Name), + }) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + + return nil, err + } + + roleIDs := []gidx.PrefixedID{} + + for { + lookup, err := lookupClient.Recv() + if err != nil { + if !errors.Is(err, io.EOF) { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + } + + break + } + + id, err := gidx.Parse(lookup.Subject.SubjectObjectId) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + + continue + } + + roleIDs = append(roleIDs, id) + } + + storageRoles, err := e.store.BatchGetRoleByID(ctx, roleIDs) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + + return nil, err + } + + roles := make([]types.Role, 0, len(storageRoles)) + + for _, r := range storageRoles { + if r.Manager != manager { + continue + } + + roles = append(roles, types.Role{ + Name: r.Name, + Manager: r.Manager, + ID: r.ID, + }) + } + + return roles, nil +} + func (e *engine) GetRoleV2(ctx context.Context, role types.Resource) (types.Role, error) { ctx, span := e.tracer.Start( ctx, @@ -213,6 +299,7 @@ func (e *engine) GetRoleV2(ctx context.Context, role types.Resource) (types.Role resp := types.Role{ ID: dbrole.ID, Name: dbrole.Name, + Manager: dbrole.Manager, Actions: actions, ResourceID: dbrole.ResourceID, @@ -338,6 +425,7 @@ func (e *engine) UpdateRoleV2(ctx context.Context, actor, roleResource types.Res } role.Name = dbRole.Name + role.Manager = dbRole.Manager role.CreatedBy = dbRole.CreatedBy role.UpdatedBy = dbRole.UpdatedBy role.ResourceID = dbRole.ResourceID diff --git a/internal/query/roles_v2_test.go b/internal/query/roles_v2_test.go index 68d0e596..a29c9bf0 100644 --- a/internal/query/roles_v2_test.go +++ b/internal/query/roles_v2_test.go @@ -130,7 +130,7 @@ func TestCreateRolesV2(t *testing.T) { } testFn := func(ctx context.Context, in input) testingx.TestResult[types.Role] { - r, err := e.CreateRoleV2(ctx, actor, in.owner, in.name, in.actions) + r, err := e.CreateRoleV2(ctx, actor, in.owner, t.Name(), in.name, in.actions) if err != nil { return testingx.TestResult[types.Role]{Err: err} } @@ -158,7 +158,7 @@ func TestGetRoleV2(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - role, err := e.CreateRoleV2(ctx, actor, tenant, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + role, err := e.CreateRoleV2(ctx, actor, tenant, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) roleRes, err := e.NewResourceFromID(role.ID) @@ -231,13 +231,13 @@ func TestListRolesV2(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - _, err = e.CreateRoleV2(ctx, actor, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + _, err = e.CreateRoleV2(ctx, actor, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) - _, err = e.CreateRoleV2(ctx, actor, root, "lb_editor", []string{"loadbalancer_list", "loadbalancer_get", "loadbalancer_update"}) + _, err = e.CreateRoleV2(ctx, actor, root, t.Name(), "lb_editor", []string{"loadbalancer_list", "loadbalancer_get", "loadbalancer_update"}) require.NoError(t, err) - _, err = e.CreateRoleV2(ctx, actor, child, "custom_role", []string{"loadbalancer_list"}) + _, err = e.CreateRoleV2(ctx, actor, child, t.Name(), "custom_role", []string{"loadbalancer_list"}) require.NoError(t, err) invalidOwner, err := e.NewResourceFromIDString("idntgrp-group") @@ -299,7 +299,7 @@ func TestUpdateRolesV2(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - role, err := e.CreateRoleV2(ctx, actor, tenant, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + role, err := e.CreateRoleV2(ctx, actor, tenant, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) roleRes, err := e.NewResourceFromID(role.ID) @@ -413,7 +413,7 @@ func TestDeleteRolesV2(t *testing.T) { actor, err := e.NewResourceFromIDString("idntusr-actor") require.NoError(t, err) - role, err := e.CreateRoleV2(ctx, subj, root, "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) + role, err := e.CreateRoleV2(ctx, subj, root, t.Name(), "lb_viewer", []string{"loadbalancer_list", "loadbalancer_get"}) require.NoError(t, err) roleRes, err := e.NewResourceFromID(role.ID) @@ -433,13 +433,13 @@ func TestDeleteRolesV2(t *testing.T) { require.NoError(t, err) // these bindings are expected to be deleted after the role is deleted - rbRoot, err := e.CreateRoleBinding(ctx, actor, root, roleRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + rbRoot, err := e.CreateRoleBinding(ctx, actor, root, roleRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) - rbChild, err := e.CreateRoleBinding(ctx, actor, child, roleRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + rbChild, err := e.CreateRoleBinding(ctx, actor, child, roleRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) - rbTheOtherChild, err := e.CreateRoleBinding(ctx, actor, theotherchild, roleRes, []types.RoleBindingSubject{{SubjectResource: subj}}) + rbTheOtherChild, err := e.CreateRoleBinding(ctx, actor, theotherchild, roleRes, t.Name(), []types.RoleBindingSubject{{SubjectResource: subj}}) require.NoError(t, err) rb, err := e.ListRoleBindings(ctx, root, &roleRes) diff --git a/internal/query/service.go b/internal/query/service.go index af600f98..8b9a6487 100644 --- a/internal/query/service.go +++ b/internal/query/service.go @@ -29,7 +29,7 @@ type Engine interface { AssignSubjectRole(ctx context.Context, subject types.Resource, role types.Role) error UnassignSubjectRole(ctx context.Context, subject types.Resource, role types.Role) error CreateRelationships(ctx context.Context, rels []types.Relationship) error - CreateRole(ctx context.Context, actor, res types.Resource, roleName string, actions []string) (types.Role, error) + CreateRole(ctx context.Context, actor, res types.Resource, manager, roleName string, actions []string) (types.Role, error) UpdateRole(ctx context.Context, actor, roleResource types.Resource, newName string, newActions []string) (types.Role, error) GetRole(ctx context.Context, roleResource types.Resource) (types.Role, error) GetRoleResource(ctx context.Context, roleResource types.Resource) (types.Resource, error) @@ -37,6 +37,7 @@ type Engine interface { ListRelationshipsFrom(ctx context.Context, resource types.Resource) ([]types.Relationship, error) ListRelationshipsTo(ctx context.Context, resource types.Resource) ([]types.Relationship, error) ListRoles(ctx context.Context, resource types.Resource) ([]types.Role, error) + ListManagerRoles(ctx context.Context, manager string, resource types.Resource) ([]types.Role, error) DeleteRelationships(ctx context.Context, relationships ...types.Relationship) error DeleteRole(ctx context.Context, roleResource types.Resource) error DeleteResourceRelationships(ctx context.Context, resource types.Resource) error @@ -47,9 +48,11 @@ type Engine interface { // v2 functions, add role bindings support // CreateRoleV2 creates a v2 role scoped to the given owner resource with the given actions. - CreateRoleV2(ctx context.Context, actor, owner types.Resource, roleName string, actions []string) (types.Role, error) + CreateRoleV2(ctx context.Context, actor, owner types.Resource, manager, roleName string, actions []string) (types.Role, error) // ListRolesV2 returns all V2 roles owned by the given resource. ListRolesV2(ctx context.Context, owner types.Resource) ([]types.Role, error) + // ListManagerRolesV2 returns all V2 roles owned by the given resource with the given manager. + ListManagerRolesV2(ctx context.Context, manager string, owner types.Resource) ([]types.Role, error) // GetRoleV2 returns a V2 role GetRoleV2(ctx context.Context, role types.Resource) (types.Role, error) // UpdateRoleV2 updates a V2 role with the given name and actions. @@ -60,10 +63,13 @@ type Engine interface { // CreateRoleBinding creates all the necessary relationships for a role binding. // role binding here establishes a three-way relationship between a role, // a resource, and the subjects. - CreateRoleBinding(ctx context.Context, actor, resource, role types.Resource, subjects []types.RoleBindingSubject) (types.RoleBinding, error) + CreateRoleBinding(ctx context.Context, actor, resource, role types.Resource, manager string, subjects []types.RoleBindingSubject) (types.RoleBinding, error) // ListRoleBindings lists all role-bindings for a resource, an optional Role // can be provided to filter the role-bindings. ListRoleBindings(ctx context.Context, resource types.Resource, optionalRole *types.Resource) ([]types.RoleBinding, error) + // ListManagerRoleBindings lists all role-bindings for a resource with the given manager, + // an optional Role can be provided to filter the role-bindings. + ListManagerRoleBindings(ctx context.Context, manager string, resource types.Resource, optionalRole *types.Resource) ([]types.RoleBinding, error) // GetRoleBinding fetches a role-binding by its ID. GetRoleBinding(ctx context.Context, rolebinding types.Resource) (types.RoleBinding, error) // UpdateRoleBinding updates the subjects of a role-binding. diff --git a/internal/storage/migrations/20241001000000_manager_fields.sql b/internal/storage/migrations/20241001000000_manager_fields.sql new file mode 100644 index 00000000..d1875a9b --- /dev/null +++ b/internal/storage/migrations/20241001000000_manager_fields.sql @@ -0,0 +1,7 @@ +-- +goose Up + +ALTER TABLE roles ADD COLUMN IF NOT EXISTS manager CHARACTER VARYING(128) NOT NULL DEFAULT ''; +ALTER TABLE rolebindings ADD COLUMN IF NOT EXISTS manager CHARACTER VARYING(128) NOT NULL DEFAULT ''; + +CREATE INDEX IF NOT EXISTS "roles_manager_resource_id" ON "roles" ("manager", "resource_id"); +CREATE INDEX IF NOT EXISTS "rolebindings_manager_resource_id" ON "rolebindings" ("manager", "resource_id"); diff --git a/internal/storage/rolebinding.go b/internal/storage/rolebinding.go index 6b85181a..fee0f51d 100644 --- a/internal/storage/rolebinding.go +++ b/internal/storage/rolebinding.go @@ -19,6 +19,10 @@ type RoleBindingService interface { // an empty slice is returned if no role bindings are found ListResourceRoleBindings(ctx context.Context, resourceID gidx.PrefixedID) ([]types.RoleBinding, error) + // ListManagerResourceRoleBindings returns all role bindings for a given resource and manager + // an empty slice is returned if no role bindings are found + ListManagerResourceRoleBindings(ctx context.Context, manager string, resourceID gidx.PrefixedID) ([]types.RoleBinding, error) + // GetRoleBindingByID returns a role binding by its prefixed ID // an ErrRoleBindingNotFound error is returned if no role binding is found GetRoleBindingByID(ctx context.Context, id gidx.PrefixedID) (types.RoleBinding, error) @@ -26,7 +30,7 @@ type RoleBindingService interface { // CreateRoleBinding creates a new role binding in the database // This method must be called with a context returned from BeginContext. // CommitContext or RollbackContext must be called afterwards if this method returns no error. - CreateRoleBinding(ctx context.Context, actorID, rbID, resourceID gidx.PrefixedID) (types.RoleBinding, error) + CreateRoleBinding(ctx context.Context, actorID, rbID, resourceID gidx.PrefixedID, manager string) (types.RoleBinding, error) // UpdateRoleBinding updates a role binding in the database // Note that this method only updates the updated_at and updated_by fields @@ -55,12 +59,13 @@ func (e *engine) GetRoleBindingByID(ctx context.Context, id gidx.PrefixedID) (ty var roleBinding types.RoleBinding err = db.QueryRowContext(ctx, ` - SELECT id, resource_id, created_by, updated_by, created_at, updated_at + SELECT id, resource_id, manager, created_by, updated_by, created_at, updated_at FROM rolebindings WHERE id = $1 `, id.String(), ).Scan( &roleBinding.ID, &roleBinding.ResourceID, + &roleBinding.Manager, &roleBinding.CreatedBy, &roleBinding.UpdatedBy, &roleBinding.CreatedAt, @@ -84,7 +89,7 @@ func (e *engine) ListResourceRoleBindings(ctx context.Context, resourceID gidx.P } rows, err := db.QueryContext(ctx, ` - SELECT id, resource_id, created_by, updated_by, created_at, updated_at + SELECT id, resource_id, manager, created_by, updated_by, created_at, updated_at FROM rolebindings WHERE resource_id = $1 ORDER BY created_at ASC `, resourceID.String(), ) @@ -101,6 +106,47 @@ func (e *engine) ListResourceRoleBindings(ctx context.Context, resourceID gidx.P err = rows.Scan( &roleBinding.ID, &roleBinding.ResourceID, + &roleBinding.Manager, + &roleBinding.CreatedBy, + &roleBinding.UpdatedBy, + &roleBinding.CreatedAt, + &roleBinding.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("%w: %s", err, resourceID.String()) + } + + roleBindings = append(roleBindings, roleBinding) + } + + return roleBindings, nil +} + +func (e *engine) ListManagerResourceRoleBindings(ctx context.Context, manager string, resourceID gidx.PrefixedID) ([]types.RoleBinding, error) { + db, err := getContextDBQuery(ctx, e) + if err != nil { + return nil, err + } + + rows, err := db.QueryContext(ctx, ` + SELECT id, resource_id, manager, created_by, updated_by, created_at, updated_at + FROM rolebindings WHERE manager = $1 AND resource_id = $1 ORDER BY created_at ASC + `, manager, resourceID.String(), + ) + if err != nil { + return nil, fmt.Errorf("%w: %s", err, resourceID.String()) + } + defer rows.Close() + + var roleBindings []types.RoleBinding + + for rows.Next() { + var roleBinding types.RoleBinding + + err = rows.Scan( + &roleBinding.ID, + &roleBinding.ResourceID, + &roleBinding.Manager, &roleBinding.CreatedBy, &roleBinding.UpdatedBy, &roleBinding.CreatedAt, @@ -116,7 +162,7 @@ func (e *engine) ListResourceRoleBindings(ctx context.Context, resourceID gidx.P return roleBindings, nil } -func (e *engine) CreateRoleBinding(ctx context.Context, actorID, rbID, resourceID gidx.PrefixedID) (types.RoleBinding, error) { +func (e *engine) CreateRoleBinding(ctx context.Context, actorID, rbID, resourceID gidx.PrefixedID, manager string) (types.RoleBinding, error) { tx, err := getContextTx(ctx) if err != nil { return types.RoleBinding{}, err @@ -125,13 +171,14 @@ func (e *engine) CreateRoleBinding(ctx context.Context, actorID, rbID, resourceI var rb types.RoleBinding err = tx.QueryRowContext(ctx, ` - INSERT INTO rolebindings (id, resource_id, created_by, updated_by, created_at, updated_at) - VALUES ($1, $2, $3, $3, $4, $4) - RETURNING id, resource_id, created_by, updated_by, created_at, updated_at - `, rbID.String(), resourceID.String(), actorID.String(), time.Now(), + INSERT INTO rolebindings (id, resource_id, manager, created_by, updated_by, created_at, updated_at) + VALUES ($1, $2, $3, $4, $4, $5, $5) + RETURNING id, resource_id, manager, created_by, updated_by, created_at, updated_at + `, rbID.String(), resourceID.String(), manager, actorID.String(), time.Now(), ).Scan( &rb.ID, &rb.ResourceID, + &rb.Manager, &rb.CreatedBy, &rb.UpdatedBy, &rb.CreatedAt, diff --git a/internal/storage/rolebinding_test.go b/internal/storage/rolebinding_test.go index 2d3e99c6..8fac7277 100644 --- a/internal/storage/rolebinding_test.go +++ b/internal/storage/rolebinding_test.go @@ -26,7 +26,7 @@ func TestGetRoleBindingByID(t *testing.T) { dbCtx, err := store.BeginContext(ctx) require.NoError(t, err, "no error expected beginning transaction context") - rb, err := store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID) + rb, err := store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID, t.Name()) require.NoError(t, err, "no error expected creating role binding") err = store.CommitContext(dbCtx) @@ -85,7 +85,7 @@ func TestListResourceRoleBindings(t *testing.T) { require.NoError(t, err, "no error expected beginning transaction context") for _, rbID := range rbIDs { - rbs[rbID], err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID) + rbs[rbID], err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID, t.Name()) require.NoError(t, err, "no error expected creating role binding") } @@ -173,7 +173,7 @@ func TestCreateRoleBinding(t *testing.T) { return result } - result.Success, result.Err = store.CreateRoleBinding(dbCtx, actorID, input, resourceID) + result.Success, result.Err = store.CreateRoleBinding(dbCtx, actorID, input, resourceID, t.Name()) if result.Err != nil { store.RollbackContext(dbCtx) //nolint:errcheck // skip check in test @@ -201,7 +201,7 @@ func TestUpdateRoleBinding(t *testing.T) { dbCtx, err := store.BeginContext(ctx) require.NoError(t, err, "no error expected beginning transaction context") - _, err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID) + _, err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID, t.Name()) require.NoError(t, err, "no error expected creating role binding") err = store.CommitContext(dbCtx) @@ -267,7 +267,7 @@ func TestDeleteRoleBinding(t *testing.T) { dbCtx, err := store.BeginContext(ctx) require.NoError(t, err, "no error expected beginning transaction context") - _, err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID) + _, err = store.CreateRoleBinding(dbCtx, actorID, rbID, resourceID, t.Name()) require.NoError(t, err, "no error expected creating role binding") err = store.CommitContext(dbCtx) diff --git a/internal/storage/roles.go b/internal/storage/roles.go index 767d063f..14c6736d 100644 --- a/internal/storage/roles.go +++ b/internal/storage/roles.go @@ -15,7 +15,8 @@ type RoleService interface { GetRoleByID(ctx context.Context, id gidx.PrefixedID) (Role, error) GetResourceRoleByName(ctx context.Context, resourceID gidx.PrefixedID, name string) (Role, error) ListResourceRoles(ctx context.Context, resourceID gidx.PrefixedID) ([]Role, error) - CreateRole(ctx context.Context, actorID gidx.PrefixedID, roleID gidx.PrefixedID, name string, resourceID gidx.PrefixedID) (Role, error) + ListManagerResourceRoles(ctx context.Context, manager string, resourceID gidx.PrefixedID) ([]Role, error) + CreateRole(ctx context.Context, actorID gidx.PrefixedID, roleID gidx.PrefixedID, name string, manager string, resourceID gidx.PrefixedID) (Role, error) UpdateRole(ctx context.Context, actorID, roleID gidx.PrefixedID, name string) (Role, error) DeleteRole(ctx context.Context, roleID gidx.PrefixedID) (Role, error) LockRoleForUpdate(ctx context.Context, roleID gidx.PrefixedID) error @@ -26,6 +27,7 @@ type RoleService interface { type Role struct { ID gidx.PrefixedID Name string + Manager string ResourceID gidx.PrefixedID CreatedBy gidx.PrefixedID UpdatedBy gidx.PrefixedID @@ -47,6 +49,7 @@ func (e *engine) GetRoleByID(ctx context.Context, id gidx.PrefixedID) (Role, err SELECT id, name, + manager, resource_id, created_by, updated_by, @@ -58,6 +61,7 @@ func (e *engine) GetRoleByID(ctx context.Context, id gidx.PrefixedID) (Role, err ).Scan( &role.ID, &role.Name, + &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, @@ -114,6 +118,7 @@ func (e *engine) GetResourceRoleByName(ctx context.Context, resourceID gidx.Pref SELECT id, name, + manager, resource_id, created_by, updated_by, @@ -129,6 +134,7 @@ func (e *engine) GetResourceRoleByName(ctx context.Context, resourceID gidx.Pref ).Scan( &role.ID, &role.Name, + &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, @@ -158,6 +164,7 @@ func (e *engine) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed SELECT id, name, + manager, resource_id, created_by, updated_by, @@ -178,7 +185,52 @@ func (e *engine) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed for rows.Next() { var role Role - if err := rows.Scan(&role.ID, &role.Name, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, &role.CreatedAt, &role.UpdatedAt); err != nil { + if err := rows.Scan(&role.ID, &role.Name, &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, &role.CreatedAt, &role.UpdatedAt); err != nil { + return nil, err + } + + roles = append(roles, role) + } + + return roles, nil +} + +// ListManagerResourceRoles retrieves all roles associated with the provided resource ID. +// If no roles are found an empty slice is returned. +func (e *engine) ListManagerResourceRoles(ctx context.Context, manager string, resourceID gidx.PrefixedID) ([]Role, error) { + db, err := getContextDBQuery(ctx, e) + if err != nil { + return nil, err + } + + rows, err := db.QueryContext(ctx, ` + SELECT + id, + name, + manager, + resource_id, + created_by, + updated_by, + created_at, + updated_at + FROM roles + WHERE + manager = $1 + AND resource_id = $2 + `, + manager, + resourceID.String(), + ) + if err != nil { + return nil, err + } + + var roles []Role + + for rows.Next() { + var role Role + + if err := rows.Scan(&role.ID, &role.Name, &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, &role.CreatedAt, &role.UpdatedAt); err != nil { return nil, err } @@ -194,7 +246,7 @@ func (e *engine) ListResourceRoles(ctx context.Context, resourceID gidx.Prefixed // // This method must be called with a context returned from BeginContext. // CommitContext or RollbackContext must be called afterwards if this method returns no error. -func (e *engine) CreateRole(ctx context.Context, actorID, roleID gidx.PrefixedID, name string, resourceID gidx.PrefixedID) (Role, error) { +func (e *engine) CreateRole(ctx context.Context, actorID, roleID gidx.PrefixedID, name string, manager string, resourceID gidx.PrefixedID) (Role, error) { tx, err := getContextTx(ctx) if err != nil { return Role{}, err @@ -204,13 +256,14 @@ func (e *engine) CreateRole(ctx context.Context, actorID, roleID gidx.PrefixedID err = tx.QueryRowContext(ctx, ` INSERT - INTO roles (id, name, resource_id, created_by, updated_by, created_at, updated_at) - VALUES ($1, $2, $3, $4, $4, now(), now()) - RETURNING id, name, resource_id, created_by, updated_by, created_at, updated_at - `, roleID.String(), name, resourceID.String(), actorID.String(), + INTO roles (id, name, manager, resource_id, created_by, updated_by, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $5, now(), now()) + RETURNING id, name, manager, resource_id, created_by, updated_by, created_at, updated_at + `, roleID.String(), name, manager, resourceID.String(), actorID.String(), ).Scan( &role.ID, &role.Name, + &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, @@ -247,11 +300,12 @@ func (e *engine) UpdateRole(ctx context.Context, actorID, roleID gidx.PrefixedID err = tx.QueryRowContext(ctx, ` UPDATE roles SET name = $1, updated_by = $2, updated_at = now() WHERE id = $3 - RETURNING id, name, resource_id, created_by, updated_by, created_at, updated_at + RETURNING id, name, manager, resource_id, created_by, updated_by, created_at, updated_at `, name, actorID.String(), roleID.String(), ).Scan( &role.ID, &role.Name, + &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, @@ -316,7 +370,7 @@ func (e *engine) BatchGetRoleByID(ctx context.Context, ids []gidx.PrefixedID) ([ inClause, args := e.buildBatchInClauseWithIDs(ids) q := fmt.Sprintf(` SELECT - id, name, resource_id, + id, name, manager, resource_id, created_by, updated_by, created_at, updated_at FROM roles WHERE id IN (%s) @@ -332,7 +386,7 @@ func (e *engine) BatchGetRoleByID(ctx context.Context, ids []gidx.PrefixedID) ([ for rows.Next() { var role Role - if err := rows.Scan(&role.ID, &role.Name, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, &role.CreatedAt, &role.UpdatedAt); err != nil { + if err := rows.Scan(&role.ID, &role.Name, &role.Manager, &role.ResourceID, &role.CreatedBy, &role.UpdatedBy, &role.CreatedAt, &role.UpdatedAt); err != nil { return nil, err } diff --git a/internal/storage/roles_test.go b/internal/storage/roles_test.go index d8dea9ba..e3dd311e 100644 --- a/internal/storage/roles_test.go +++ b/internal/storage/roles_test.go @@ -29,7 +29,7 @@ func TestGetRoleByID(t *testing.T) { dbCtx, err := store.BeginContext(ctx) require.NoError(t, err, "no error expected beginning transaction context") - createdRole, err := store.CreateRole(dbCtx, actorID, roleID, roleName, resourceID) + createdRole, err := store.CreateRole(dbCtx, actorID, roleID, roleName, t.Name(), resourceID) require.NoError(t, err, "no error expected while seeding database role") err = store.CommitContext(dbCtx) @@ -93,7 +93,7 @@ func TestListResourceRoles(t *testing.T) { require.NoError(t, err, "no error expected beginning transaction context") for roleName, roleID := range groups { - _, err := store.CreateRole(dbCtx, actorID, roleID, roleName, resourceID) + _, err := store.CreateRole(dbCtx, actorID, roleID, roleName, t.Name(), resourceID) require.NoError(t, err, "no error expected creating role", roleName) } @@ -216,7 +216,7 @@ func TestCreateRole(t *testing.T) { return result } - result.Success, result.Err = store.CreateRole(dbCtx, actorID, input.id, input.name, resourceID) + result.Success, result.Err = store.CreateRole(dbCtx, actorID, input.id, input.name, t.Name(), resourceID) if result.Err != nil { store.RollbackContext(dbCtx) //nolint:errcheck // skip check in test @@ -251,10 +251,10 @@ func TestUpdateRole(t *testing.T) { require.NoError(t, err, "no error expected beginning transaction context") - createdDBRole1, err := store.CreateRole(dbCtx, actorID, role1ID, role1Name, resourceID) + createdDBRole1, err := store.CreateRole(dbCtx, actorID, role1ID, role1Name, t.Name(), resourceID) require.NoError(t, err, "no error expected while seeding database role") - _, err = store.CreateRole(dbCtx, actorID, role2ID, role2Name, resourceID) + _, err = store.CreateRole(dbCtx, actorID, role2ID, role2Name, t.Name(), resourceID) require.NoError(t, err, "no error expected while seeding database role 2") err = store.CommitContext(dbCtx) @@ -361,7 +361,7 @@ func TestDeleteRole(t *testing.T) { require.NoError(t, err, "no error expected beginning transaction context") - createdDBRole, err := store.CreateRole(dbCtx, actorID, roleID, roleName, resourceID) + createdDBRole, err := store.CreateRole(dbCtx, actorID, roleID, roleName, t.Name(), resourceID) require.NoError(t, err, "no error expected while seeding database role") diff --git a/internal/types/types.go b/internal/types/types.go index ff7d0509..fc6d929c 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -11,6 +11,7 @@ import ( type Role struct { ID gidx.PrefixedID Name string + Manager string Actions []string ResourceID gidx.PrefixedID @@ -97,6 +98,7 @@ type Relationship struct { type RoleBinding struct { ID gidx.PrefixedID ResourceID gidx.PrefixedID + Manager string RoleID gidx.PrefixedID SubjectIDs []gidx.PrefixedID diff --git a/openapi-v2.yaml b/openapi-v2.yaml index 315486e0..6bab49f7 100644 --- a/openapi-v2.yaml +++ b/openapi-v2.yaml @@ -20,6 +20,8 @@ paths: list all available roles for a resource, including roles that are inherited from parent resources operationId: listRoles + parameters: + - $ref: '#/components/parameters/manager' responses: "200": description: tnntten-root @@ -72,6 +74,9 @@ paths: name: type: string example: super_user + manager: + type: string + example: service-a updated_at: type: string example: "2024-04-10T20:18:10Z" @@ -166,6 +171,9 @@ paths: name: type: string example: lb_editor + manager: + type: string + example: service-a examples: create-role: value: @@ -175,6 +183,7 @@ paths: - loadbalancer_update - loadbalancer_create name: lb_editor + manager: svc-access-manager responses: "201": description: "role object" @@ -214,6 +223,9 @@ paths: name: type: string example: super_admin + manager: + type: string + example: svc-access-manager resource_id: type: string example: tnntten-root @@ -235,52 +247,46 @@ paths: created_by: idntusr-bailin id: permrv2-IG7RfsYhyga0EwEbY4BKs name: lb_editor + manager: svc-access-manager resource_id: tnntten-a updated_at: "2024-02-29T18:18:18Z" updated_by: idntusr-bailin - lb-viwer: + lb-viewer: value: actions: - - role_list - - rolebinding_create - loadbalancer_list - - loadbalancer_update - - loadbalancer_delete - - role_create - - role_delete - - loadbalancer_create - loadbalancer_get - - rolebinding_list - - rolebinding_delete - - role_get - - role_update created_at: "2024-02-28T17:22:04Z" created_by: idntusr-bailin - id: permrv2-ecBlNMsPrvVFgUUAUfmeY - name: super_admin + id: permrv2-ecBlNMsYhyga0EwEUffsY + name: lb_viewer + manager: svc-access-manager resource_id: tnntten-root updated_at: "2024-02-28T17:22:04Z" updated_by: idntusr-bailin super-admin: value: actions: - - role_list - - rolebinding_create + - loadbalancer_create + - loadbalancer_delete + - loadbalancer_get - loadbalancer_list - loadbalancer_update - - loadbalancer_delete - role_create - role_delete - - loadbalancer_create - - loadbalancer_get - - rolebinding_list - - rolebinding_delete - role_get + - role_list - role_update + - iam_rolebinding_create + - iam_rolebinding_delete + - iam_rolebinding_get + - iam_rolebinding_list + - iam_rolebinding_update created_at: "2024-02-28T17:22:04Z" created_by: idntusr-bailin id: permrv2-ecBlNMsPrvVFgUUAUfmeY name: super_admin + manager: svc-access-manager resource_id: tnntten-root updated_at: "2024-02-28T17:22:04Z" updated_by: idntusr-bailin @@ -396,6 +402,9 @@ paths: name: type: string example: super_user + manager: + type: string + example: service-a resource_id: type: string example: tnntten-root @@ -699,6 +708,7 @@ paths: an optional query parameter `role_id` can be used to filter the results. operationId: listRoleBindings parameters: + - $ref: '#/components/parameters/manager' - name: role_id in: query schema: @@ -826,6 +836,9 @@ paths: schema: type: object properties: + manager: + type: string + example: service-a role_id: type: string example: permrv2-PLjILDwe8kG_t42tMCDiB @@ -840,6 +853,7 @@ paths: create-role-binding: value: role_id: permrv2-PLjILDwe8kG_t42tMCDiB + manager: svc-access-manager subject_ids: - idntgrp-my-subgroup responses: @@ -1154,7 +1168,15 @@ paths: - iam_rolebinding_create - iam_rolebinding_update - iam_rolebinding_delete + components: + parameters: + manager: + in: query + name: manager + required: false + schema: + type: string securitySchemes: oauth2: type: oauth2