Files
threshold/internal/iam/repository_role_grant.go
T
Jeff Mitchell 62d4f8453b Don't explode grants from the DB (#5104)
This removes the Cartesian product in the DB for GrantsForUser in favor
of returning the actual grant scope information and dealing with it in
application code.

(cherry picked from commit 913cd536823d73994acc4f4d833208c1cc05f91b)
2024-09-24 14:18:53 +00:00

497 lines
18 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package iam
import (
"context"
"fmt"
"sort"
"strings"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/oplog"
"github.com/hashicorp/boundary/internal/perms"
)
// AddRoleGrant will add role grants associated with the role ID in the
// repository. No options are currently supported. Zero is not a valid value for
// the WithVersion option and will return an error.
func (r *Repository) AddRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, _ ...Option) ([]*RoleGrant, error) {
const op = "iam.(Repository).AddRoleGrants"
if roleId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if len(grants) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
if roleVersion == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
newRoleGrants := make([]*RoleGrant, 0, len(grants))
for _, grant := range grants {
roleGrant, err := NewRoleGrant(ctx, roleId, grant)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant"))
}
newRoleGrants = append(newRoleGrants, roleGrant)
}
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
// We need to update the role version as that's the aggregate
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = uint32(roleVersion) + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
roleGrantOplogMsgs := make([]*oplog.Message, 0, len(newRoleGrants))
if err := w.CreateItems(ctx, newRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grants"))
}
msgs = append(msgs, roleGrantOplogMsgs...)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return newRoleGrants, nil
}
// DeleteRoleGrants deletes grants (as strings) from a role (roleId). The role's
// current db version must match the roleVersion or an error will be returned.
// Zero is not a valid value for the WithVersion option and will return an
// error.
func (r *Repository) DeleteRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, _ ...Option) (int, error) {
const op = "iam.(Repository).DeleteRoleGrants"
if roleId == "" {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if len(grants) == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
if roleVersion == 0 {
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
role := allocRole()
role.PublicId = roleId
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope to create metadata", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var totalRowsDeleted int
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = uint32(roleVersion) + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
// Find existing grants
roleGrants := []*RoleGrant{}
if err := reader.SearchWhere(ctx, &roleGrants, "role_id = ?", []any{roleId}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grants"))
}
found := map[string]bool{}
for _, rg := range roleGrants {
found[rg.CanonicalGrant] = true
}
// Check incoming grants to see if they exist and if so add to
// delete slice
deleteRoleGrants := make([]*RoleGrant, 0, len(grants))
for _, grant := range grants {
// Use a fake scope, just want to get out a canonical string
perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant}, perms.WithSkipFinalValidation(true))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string"))
}
// We don't have what they want to delete, so ignore it
if !found[perm.CanonicalString()] {
continue
}
roleGrant, err := NewRoleGrant(ctx, roleId, grant)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant"))
}
deleteRoleGrants = append(deleteRoleGrants, roleGrant)
}
if len(deleteRoleGrants) == 0 {
return nil
}
roleGrantOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrants))
rowsDeleted, err := w.DeleteItems(ctx, deleteRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete role grant"))
}
if rowsDeleted != len(deleteRoleGrants) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grants deleted %d did not match request for %d", rowsDeleted, len(deleteRoleGrants)))
}
totalRowsDeleted = rowsDeleted
msgs = append(msgs, roleGrantOplogMsgs...)
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
return nil
},
)
if err != nil {
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return totalRowsDeleted, nil
}
// SetRoleGrants sets grants on a role (roleId). The role's current db version
// must match the roleVersion or an error will be returned. Zero is not a valid
// value for the WithVersion option and will return an error.
func (r *Repository) SetRoleGrants(ctx context.Context, roleId string, roleVersion uint32, grants []string, _ ...Option) ([]*RoleGrant, int, error) {
const op = "iam.(Repository).SetRoleGrants"
if roleId == "" {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
if roleVersion == 0 {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
}
// Explicitly set to zero clears, but treat nil as a mistake
if grants == nil {
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing grants")
}
role := allocRole()
role.PublicId = roleId
// TODO(mgaffney) 08/2020: Use SQL to calculate changes.
// NOTE: Set calculation can safely take place out of the transaction since
// we are using roleVersion to ensure that we end up operating on the same
// set of data from this query to the final set in the transaction function
// Find existing grants
roleGrants := []*RoleGrant{}
if err := r.reader.SearchWhere(ctx, &roleGrants, "role_id = ?", []any{roleId}); err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to search for grants"))
}
found := map[string]*RoleGrant{}
for _, rg := range roleGrants {
found[rg.CanonicalGrant] = rg
}
// Check incoming grants to see if they exist and if so act appropriately
currentRoleGrants := make([]*RoleGrant, 0, len(grants)+len(found))
addRoleGrants := make([]*RoleGrant, 0, len(grants))
deleteRoleGrants := make([]*RoleGrant, 0, len(grants))
for _, grant := range grants {
// Use a fake scope, just want to get out a canonical string
perm, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant}, perms.WithSkipFinalValidation(true))
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("error parsing grant string"))
}
canonicalString := perm.CanonicalString()
rg, ok := found[canonicalString]
if ok {
// If we have an exact match, do nothing, we want to keep
// it, but remove from found
currentRoleGrants = append(currentRoleGrants, rg)
delete(found, canonicalString)
continue
}
// Not found, so add
rg, err = NewRoleGrant(ctx, roleId, grant)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create in memory role grant"))
}
addRoleGrants = append(addRoleGrants, rg)
currentRoleGrants = append(currentRoleGrants, rg)
}
if len(found) > 0 {
for _, rg := range found {
deleteRoleGrants = append(deleteRoleGrants, rg)
}
}
if len(addRoleGrants) == 0 && len(deleteRoleGrants) == 0 {
return currentRoleGrants, db.NoRowsAffected, nil
}
scope, err := role.GetScope(ctx, r.reader)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to get role %s scope", roleId)))
}
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
}
var totalRowsDeleted int
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, w db.Writer) error {
msgs := make([]*oplog.Message, 0, 2)
roleTicket, err := w.GetTicket(ctx, &role)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
}
updatedRole := allocRole()
updatedRole.PublicId = roleId
updatedRole.Version = roleVersion + 1
var roleOplogMsg oplog.Message
rowsUpdated, err := w.Update(ctx, &updatedRole, []string{"Version"}, nil, db.NewOplogMsg(&roleOplogMsg), db.WithVersion(&roleVersion))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update role version"))
}
if rowsUpdated != 1 {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated role and %d rows updated", rowsUpdated))
}
msgs = append(msgs, &roleOplogMsg)
// Write the new ones in
if len(addRoleGrants) > 0 {
roleGrantOplogMsgs := make([]*oplog.Message, 0, len(addRoleGrants))
if err := w.CreateItems(ctx, addRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs)); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add grants during set"))
}
msgs = append(msgs, roleGrantOplogMsgs...)
}
// Anything we didn't take out of found needs to be removed
if len(deleteRoleGrants) > 0 {
roleGrantOplogMsgs := make([]*oplog.Message, 0, len(deleteRoleGrants))
rowsDeleted, err := w.DeleteItems(ctx, deleteRoleGrants, db.NewOplogMsgs(&roleGrantOplogMsgs))
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete role grant"))
}
if rowsDeleted != len(deleteRoleGrants) {
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("role grants deleted %d did not match request for %d", rowsDeleted, len(deleteRoleGrants)))
}
totalRowsDeleted = rowsDeleted
msgs = append(msgs, roleGrantOplogMsgs...)
}
metadata := oplog.Metadata{
"op-type": []string{oplog.OpType_OP_TYPE_DELETE.String(), oplog.OpType_OP_TYPE_CREATE.String()},
"scope-id": []string{scope.PublicId},
"scope-type": []string{scope.Type},
"resource-public-id": []string{roleId},
}
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, roleTicket, metadata, msgs); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
}
currentRoleGrants, err = r.ListRoleGrants(ctx, roleId)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to retrieve current role grants after set"))
}
return nil
},
)
if err != nil {
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
}
return currentRoleGrants, totalRowsDeleted, nil
}
// ListRoleGrants returns the grants for the roleId and supports the WithLimit
// option.
func (r *Repository) ListRoleGrants(ctx context.Context, roleId string, opt ...Option) ([]*RoleGrant, error) {
const op = "iam.(Repository).ListRoleGrants"
if roleId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role id")
}
var roleGrants []*RoleGrant
if err := r.list(ctx, &roleGrants, "role_id = ?", []any{roleId}, opt...); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup role grants"))
}
return roleGrants, nil
}
// ListRoleGrantScopes returns the grant scopes for the roleId and supports the WithLimit
// option.
func (r *Repository) ListRoleGrantScopes(ctx context.Context, roleIds []string, opt ...Option) ([]*RoleGrantScope, error) {
const op = "iam.(Repository).ListRoleGrantScopes"
if len(roleIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
query := "?"
var args []any
for i, roleId := range roleIds {
if roleId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing role ids")
}
if i > 0 {
query = query + ", ?"
}
args = append(args, roleId)
}
var roleGrantScopes []*RoleGrantScope
if err := r.list(ctx, &roleGrantScopes, fmt.Sprintf("role_id in (%s)", query), args, opt...); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup role grant scopes"))
}
return roleGrantScopes, nil
}
type multiGrantTuple struct {
RoleId string
RoleScopeId string
RoleParentScopeId string
GrantScopeIds string
Grants string
}
func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Option) (perms.GrantTuples, error) {
const op = "iam.(Repository).GrantsForUser"
if userId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing user id")
}
opts := getOpts(opt...)
const (
anonUser = `where public_id in (?)`
authUser = `where public_id in ('u_anon', 'u_auth', ?)`
)
var query string
switch userId {
case globals.AnonymousUserId:
query = fmt.Sprintf(grantsForUserQuery, anonUser)
default:
query = fmt.Sprintf(grantsForUserQuery, authUser)
}
var grants []multiGrantTuple
rows, err := r.reader.Query(ctx, query, []any{userId})
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
defer rows.Close()
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &grants); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
}
if err := rows.Err(); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
ret := make(perms.GrantTuples, 0, len(grants)*3)
for _, grant := range grants {
for _, grantScopeId := range strings.Split(grant.GrantScopeIds, "^") {
for _, canonicalGrant := range strings.Split(grant.Grants, "^") {
gt := perms.GrantTuple{
RoleId: grant.RoleId,
RoleScopeId: grant.RoleScopeId,
RoleParentScopeId: grant.RoleParentScopeId,
GrantScopeId: grantScopeId,
Grant: canonicalGrant,
}
if gt.GrantScopeId == globals.GrantScopeThis || gt.GrantScopeId == "" {
gt.GrantScopeId = grant.RoleScopeId
}
ret = append(ret, gt)
}
}
}
if opts.withTestCacheMultiGrantTuples != nil {
for i, grant := range grants {
grant.testStableSort()
grants[i] = grant
}
*opts.withTestCacheMultiGrantTuples = grants
}
return ret, nil
}
func (m *multiGrantTuple) testStableSort() {
grantScopeIds := strings.Split(m.GrantScopeIds, "^")
sort.Strings(grantScopeIds)
m.GrantScopeIds = strings.Join(grantScopeIds, "^")
gts := strings.Split(m.Grants, "^")
sort.Strings(gts)
m.Grants = strings.Join(gts, "^")
}