* Update host set member test * Add ListSetMembers method signature and doc * Move repository methods for host set members to new file * Add TestSetMembers * Implement ListSetMembers * Return hosts from ListSetMembers * Implement AddSetMembers * Implement DeleteSetMembers * Use sql.QueryContext not sql.Query * Implement SetSetMembers * Refactor: extract getHosts method * Refactor: extract createMembers method * Refactor: extract common functions * Return correct count of changed In SetSetMembers, retrieving the current list of hosts in the set happens after the transaction to modify the set has completed. This retrieval can result in an error but the number of modified rows is not effected. * Make list set members an unexported method * Get hosts within the same transaction as mutations of the set * Return hosts of set from LookupSet method * Return hosts of set from UpdateSet method * Remove listSetMembers * Fix merge conflict * Add NOTE and TODOs about using new pattern in "SetMembers" methods
469 lines
16 KiB
Go
469 lines
16 KiB
Go
package iam
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/boundary/internal/db"
|
|
"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, opt ...Option) ([]*RoleGrant, error) {
|
|
if roleId == "" {
|
|
return nil, fmt.Errorf("add role grants: missing role id %w", db.ErrInvalidParameter)
|
|
}
|
|
if len(grants) == 0 {
|
|
return nil, fmt.Errorf("add role grants: missing grants: %w", db.ErrInvalidParameter)
|
|
}
|
|
if roleVersion == 0 {
|
|
return nil, fmt.Errorf("add role grants: version cannot be zero: %w", db.ErrInvalidParameter)
|
|
}
|
|
role := allocRole()
|
|
role.PublicId = roleId
|
|
|
|
newRoleGrants := make([]interface{}, 0, len(grants))
|
|
for _, grant := range grants {
|
|
roleGrant, err := NewRoleGrant(roleId, grant)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("add role grants: unable to create in memory role grant: %w", err)
|
|
}
|
|
newRoleGrants = append(newRoleGrants, roleGrant)
|
|
}
|
|
|
|
scope, err := role.GetScope(ctx, r.reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("add role grants: unable to get role %s scope: %w", roleId, err)
|
|
}
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("add role grants: unable to get oplog wrapper: %w", err)
|
|
}
|
|
|
|
_, 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(&role)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to get ticket: %w", err)
|
|
}
|
|
|
|
// 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 fmt.Errorf("unable to update role version: %w", err)
|
|
}
|
|
if rowsUpdated != 1 {
|
|
return fmt.Errorf("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 fmt.Errorf("unable to add grants: %w", err)
|
|
}
|
|
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 fmt.Errorf("unable to write oplog: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("add role grants: error creating grants: %w", err)
|
|
}
|
|
roleGrants := make([]*RoleGrant, 0, len(newRoleGrants))
|
|
for _, grant := range newRoleGrants {
|
|
roleGrants = append(roleGrants, grant.(*RoleGrant))
|
|
}
|
|
return roleGrants, 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, opt ...Option) (int, error) {
|
|
if roleId == "" {
|
|
return 0, fmt.Errorf("delete role grants: missing role id %w", db.ErrInvalidParameter)
|
|
}
|
|
if len(grants) == 0 {
|
|
return 0, fmt.Errorf("delete role grants: missing grants: %w", db.ErrInvalidParameter)
|
|
}
|
|
if roleVersion == 0 {
|
|
return 0, fmt.Errorf("delete role grants: version cannot be zero: %w", db.ErrInvalidParameter)
|
|
}
|
|
role := allocRole()
|
|
role.PublicId = roleId
|
|
|
|
scope, err := role.GetScope(ctx, r.reader)
|
|
if err != nil {
|
|
return db.NoRowsAffected, fmt.Errorf("delete role grants: unable to get role %s scope to create metadata: %w", roleId, err)
|
|
}
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return db.NoRowsAffected, fmt.Errorf("delete role grants: unable to get oplog wrapper: %w", err)
|
|
}
|
|
|
|
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(&role)
|
|
if err != nil {
|
|
return fmt.Errorf("delete role grants: unable to get ticket: %w", err)
|
|
}
|
|
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 fmt.Errorf("delete role grants: unable to update role version: %w", err)
|
|
}
|
|
if rowsUpdated != 1 {
|
|
return fmt.Errorf("delete roles grants: updated role and %d rows updated", rowsUpdated)
|
|
}
|
|
msgs = append(msgs, &roleOplogMsg)
|
|
|
|
// Find existing grants
|
|
roleGrants := []*RoleGrant{}
|
|
if err := reader.SearchWhere(ctx, &roleGrants, "role_id = ?", []interface{}{roleId}); err != nil {
|
|
return fmt.Errorf("delete role grants: unable to search for grants: %w", err)
|
|
}
|
|
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([]interface{}, 0, len(grants))
|
|
for _, grant := range grants {
|
|
// Use a fake scope, just want to get out a canonical string
|
|
perm, err := perms.Parse("o_abcd1234", "", grant)
|
|
if err != nil {
|
|
return fmt.Errorf("delete role grants: error parsing grant string: %w", err)
|
|
}
|
|
// We don't have what they want to delete, so ignore it
|
|
if !found[perm.CanonicalString()] {
|
|
continue
|
|
}
|
|
|
|
roleGrant, err := NewRoleGrant(roleId, grant)
|
|
if err != nil {
|
|
return fmt.Errorf("delete role grants: unable to create in memory role grant: %w", err)
|
|
}
|
|
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 fmt.Errorf("delete role grants: unable to delete role grant: %w", err)
|
|
}
|
|
if rowsDeleted != len(deleteRoleGrants) {
|
|
return fmt.Errorf("delete role grants: 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 fmt.Errorf("delete role grants: unable to write oplog: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return db.NoRowsAffected, fmt.Errorf("delete role grants: error deleting role grants: %w", err)
|
|
}
|
|
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, opt ...Option) ([]*RoleGrant, int, error) {
|
|
if roleId == "" {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: missing role id %w", db.ErrInvalidParameter)
|
|
}
|
|
if roleVersion == 0 {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: version cannot be zero: %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
// Explicitly set to zero clears, but treat nil as a mistake
|
|
if grants == nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: nil grants: %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
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 = ?", []interface{}{roleId}); err != nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: unable to search for grants: %w", err)
|
|
}
|
|
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([]interface{}, 0, len(grants))
|
|
deleteRoleGrants := make([]interface{}, 0, len(grants))
|
|
for _, grant := range grants {
|
|
// Use a fake scope, just want to get out a canonical string
|
|
perm, err := perms.Parse("o_abcd1234", "", grant)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: error parsing grant string: %w", err)
|
|
}
|
|
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(roleId, grant)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: unable to create in memory role grant: %w", err)
|
|
}
|
|
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, fmt.Errorf("set role grants: unable to get role %s scope: %w", roleId, err)
|
|
}
|
|
oplogWrapper, err := r.kms.GetWrapper(ctx, scope.GetPublicId(), kms.KeyPurposeOplog)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: unable to get oplog wrapper: %w", err)
|
|
}
|
|
|
|
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(&role)
|
|
if err != nil {
|
|
return fmt.Errorf("set role grants: unable to get ticket: %w", err)
|
|
}
|
|
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 fmt.Errorf("set role grants: unable to update role version: %w", err)
|
|
}
|
|
if rowsUpdated != 1 {
|
|
return fmt.Errorf("set roles grants: 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 fmt.Errorf("unable to add grants during set: %w", err)
|
|
}
|
|
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 fmt.Errorf("set role grants: unable to delete role grant: %w", err)
|
|
}
|
|
if rowsDeleted != len(deleteRoleGrants) {
|
|
return fmt.Errorf("set role grants: 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 fmt.Errorf("set role grants: unable to write oplog: %w", err)
|
|
}
|
|
|
|
currentRoleGrants, err = r.ListRoleGrants(ctx, roleId)
|
|
if err != nil {
|
|
return fmt.Errorf("set role grants: unable to retrieve current role grants after set: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, db.NoRowsAffected, fmt.Errorf("set role grants: error set role grants: %w", err)
|
|
}
|
|
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) {
|
|
if roleId == "" {
|
|
return nil, fmt.Errorf("add role grants: missing role id %w", db.ErrInvalidParameter)
|
|
}
|
|
var roleGrants []*RoleGrant
|
|
if err := r.list(ctx, &roleGrants, "role_id = ?", []interface{}{roleId}, opt...); err != nil {
|
|
return nil, fmt.Errorf("lookup role grants: unable to lookup role grants: %w", err)
|
|
}
|
|
return roleGrants, nil
|
|
}
|
|
|
|
func (r *Repository) GrantsForUser(ctx context.Context, userId string, opt ...Option) ([]perms.GrantPair, error) {
|
|
if userId == "" {
|
|
return nil, fmt.Errorf("get grants for user: missing user id: %w", db.ErrInvalidParameter)
|
|
}
|
|
|
|
const (
|
|
anonUser = `where public_id in ($1)`
|
|
authUser = `where public_id in ('u_anon', 'u_auth', $1)`
|
|
grantsQuery = `
|
|
with
|
|
users (id) as (
|
|
select public_id
|
|
from iam_user
|
|
%s -- anonUser || authUser
|
|
),
|
|
user_groups (id) as (
|
|
select group_id
|
|
from iam_group_member_user,
|
|
users
|
|
where member_id in (users.id)
|
|
),
|
|
group_roles (role_id) as (
|
|
select role_id
|
|
from iam_group_role,
|
|
user_groups
|
|
where principal_id in (user_groups.id)
|
|
),
|
|
user_roles (role_id) as (
|
|
select role_id
|
|
from iam_user_role,
|
|
users
|
|
where principal_id in (users.id)
|
|
),
|
|
user_group_roles (role_id) as (
|
|
select role_id
|
|
from group_roles
|
|
union
|
|
select role_id
|
|
from user_roles
|
|
),
|
|
roles (role_id, grant_scope_id) as (
|
|
select iam_role.public_id,
|
|
iam_role.grant_scope_id
|
|
from iam_role,
|
|
user_group_roles
|
|
where public_id in (user_group_roles.role_id)
|
|
),
|
|
final (role_scope, role_grant) as (
|
|
select roles.grant_scope_id,
|
|
iam_role_grant.canonical_grant
|
|
from roles
|
|
inner
|
|
join iam_role_grant
|
|
on roles.role_id = iam_role_grant.role_id
|
|
)
|
|
select role_scope as scope_id, role_grant as grant from final;
|
|
`
|
|
)
|
|
|
|
var query string
|
|
switch userId {
|
|
case "u_anon":
|
|
query = fmt.Sprintf(grantsQuery, anonUser)
|
|
default:
|
|
query = fmt.Sprintf(grantsQuery, authUser)
|
|
}
|
|
|
|
var grants []perms.GrantPair
|
|
tx, err := r.reader.DB()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := tx.Query(query, userId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var g perms.GrantPair
|
|
if err := r.reader.ScanRows(rows, &g); err != nil {
|
|
return nil, err
|
|
}
|
|
grants = append(grants, g)
|
|
}
|
|
return grants, nil
|
|
}
|