2022-06-05 16:43:02 -04:00
// Copyright (c) HashiCorp, Inc.
2021-07-29 18:52:56 -06:00
// SPDX-License-Identifier: BUSL-1.1
2023-02-02 17:15:17 -08:00
2020-06-10 17:58:52 -04:00
package perms
import (
2023-06-06 16:13:44 -04:00
"context"
2024-04-17 08:26:46 -07:00
"encoding/binary"
2020-06-10 17:58:52 -04:00
"encoding/json"
"fmt"
2024-04-17 08:26:46 -07:00
"hash"
"hash/fnv"
2020-06-10 17:58:52 -04:00
"sort"
"strings"
2023-01-06 17:27:19 -06:00
"unicode"
2020-06-10 17:58:52 -04:00
2022-10-28 15:52:06 -04:00
"github.com/hashicorp/boundary/globals"
2021-01-13 15:50:07 -08:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-10 17:15:02 -04:00
"github.com/hashicorp/boundary/internal/types/action"
"github.com/hashicorp/boundary/internal/types/resource"
"github.com/hashicorp/boundary/internal/types/scope"
2023-06-06 16:13:44 -04:00
"golang.org/x/exp/slices"
2020-06-10 17:58:52 -04:00
)
2024-09-17 10:07:43 -04:00
type ActionSet map [ action . Type ] bool
2023-06-06 16:13:44 -04:00
// Actions is a helper that goes through the map and returns both the actual
// types of actions as a slice and the equivalent strings
2024-09-17 10:07:43 -04:00
func ( a ActionSet ) Actions () ( typs [] action . Type , strs [] string ) {
2023-06-06 16:13:44 -04:00
typs = make ([] action . Type , 0 , len ( a ))
strs = make ([] string , 0 , len ( a ))
for k , v := range a {
// Nothing should be in there if not true, but doesn't hurt to validate
if ! v {
continue
}
typs = append ( typs , k )
strs = append ( strs , k . String ())
}
return
}
2021-06-07 09:19:21 -05:00
// GrantTuple is simply a struct that can be reference from other code to return
2020-07-14 14:32:47 -07:00
// a set of scopes and grants to parse
2021-06-07 09:19:21 -05:00
type GrantTuple struct {
2024-09-17 10:07:43 -04:00
RoleId string
RoleScopeId string
RoleParentScopeId string
GrantScopeId string
Grant string
2020-07-14 14:32:47 -07:00
}
2024-04-17 08:26:46 -07:00
type GrantTuples [] GrantTuple
// GrantsHash returns a stable hash of all the grants in the GrantTuples.
2024-12-06 16:45:38 -05:00
//
// In the event that GrantTuples is nil, return a slice of empty bytes: []byte{0,0,0,0,0,0,0,0}
2024-04-17 08:26:46 -07:00
func ( g GrantTuples ) GrantHash ( ctx context . Context ) ([] byte , error ) {
const op = "perms.(GrantTuples).GrantHash"
var values [] string
for _ , grant := range g {
2024-09-17 10:07:43 -04:00
values = append ( values , grant . Grant , grant . RoleId , grant . GrantScopeId )
2024-04-17 08:26:46 -07:00
}
// Sort for deterministic output
slices . Sort ( values )
hashVal , err := hashStrings ( values ... )
if err != nil {
return nil , errors . Wrap ( ctx , err , op )
}
return binary . LittleEndian . AppendUint64 ( make ([] byte , 0 , 4 ), hashVal ), nil
}
func hashStrings ( s ... string ) ( uint64 , error ) {
hasher := fnv . New64 ()
var h uint64
var err error
for _ , current := range s {
hasher . Reset ()
if _ , err = hasher . Write ([] byte ( current )); err != nil {
return 0 , err
}
if h , err = hashUpdateOrdered ( hasher , h , hasher . Sum64 ()); err != nil {
return 0 , err
}
}
return h , nil
}
// hashUpdateOrdered is taken directly from
// https://github.com/mitchellh/hashstructure
func hashUpdateOrdered ( h hash . Hash64 , a , b uint64 ) ( uint64 , error ) {
// For ordered updates, use a real hash function
h . Reset ()
e1 := binary . Write ( h , binary . LittleEndian , a )
e2 := binary . Write ( h , binary . LittleEndian , b )
if e1 != nil {
return 0 , e1
}
if e2 != nil {
return 0 , e2
}
return h . Sum64 (), nil
}
2020-06-10 17:58:52 -04:00
// Scope provides an in-memory representation of iam.Scope without the
// underlying storage references or capabilities.
type Scope struct {
// Id is the public id of the iam.Scope
Id string
2024-09-17 10:07:43 -04:00
// ParentId is the parent scope ID
ParentId string
2020-06-10 17:58:52 -04:00
}
// Grant is a Go representation of a parsed grant
type Grant struct {
2024-09-17 10:07:43 -04:00
// The role scope ID
roleScopeId string
// The role's parent scope ID, if any
roleParentScopeId string
2020-06-10 17:58:52 -04:00
2023-06-06 16:13:44 -04:00
// The ID of the grant, if provided. Deprecated in favor of ids.
2020-06-10 17:58:52 -04:00
id string
2023-06-06 16:13:44 -04:00
// The IDs in the grant, if provided
ids [] string
2024-09-17 10:07:43 -04:00
// The grant scope ID of the grant
grantScopeId string
2020-06-10 17:58:52 -04:00
// The type, if provided
2020-06-26 11:00:14 -04:00
typ resource . Type
2020-06-10 17:58:52 -04:00
// The set of actions being granted
2024-09-17 10:07:43 -04:00
actions ActionSet
2020-06-10 17:58:52 -04:00
2021-05-04 19:57:35 -05:00
// The set of output fields granted
2023-01-06 17:27:19 -06:00
OutputFields * OutputFields
2021-05-04 19:57:35 -05:00
2020-06-10 17:58:52 -04:00
// This is used as a temporary staging area before validating permissions to
// allow the same validation code across grant string formats
actionsBeingParsed [] string
}
2023-06-06 16:13:44 -04:00
// Id returns the ID the grant refers to, if any
2020-07-16 22:18:27 -04:00
func ( g Grant ) Id () string {
return g . id
}
2023-06-06 16:13:44 -04:00
// Ids returns the IDs the grant refers to, if any
func ( g Grant ) Ids () [] string {
return g . ids
}
2024-09-17 10:07:43 -04:00
// GrantScopeId returns the grant scope ID the grant refers to, if any
func ( g Grant ) GrantScopeId () string {
return g . grantScopeId
}
2023-06-06 16:13:44 -04:00
// Type returns the type the grant refers to, or Unknown
2020-07-16 22:18:27 -04:00
func ( g Grant ) Type () resource . Type {
return g . typ
}
2023-06-06 16:13:44 -04:00
// Actions returns the actions as a slice from the internal map, along with the
// string representations of those actions.
func ( g Grant ) Actions () ([] action . Type , [] string ) {
return g . actions . Actions ()
2020-07-16 22:18:27 -04:00
}
2022-06-05 16:43:02 -04:00
// hasActionOrSubaction checks whether a grant's action set contains the given
// action or contains an action that is a subaction of the passed-in parameter.
// This is used for validation checking of parsed grants. N.B.: this is the
// opposite check of action.Type.IsActionOrParent, which is why the ordering is
// reversed going into that call.
func ( g Grant ) hasActionOrSubaction ( act action . Type ) bool {
for k := range g . actions {
if act . IsActionOrParent ( k ) {
return true
}
}
return false
}
2020-06-10 17:58:52 -04:00
func ( g Grant ) clone () * Grant {
ret := & Grant {
2024-09-17 10:07:43 -04:00
roleScopeId : g . roleScopeId ,
roleParentScopeId : g . roleParentScopeId ,
id : g . id ,
ids : g . ids ,
grantScopeId : g . grantScopeId ,
typ : g . typ ,
2020-06-10 17:58:52 -04:00
}
2023-06-06 16:13:44 -04:00
if g . ids != nil {
ret . ids = make ([] string , len ( g . ids ))
copy ( ret . ids , g . ids )
}
2020-06-10 17:58:52 -04:00
if g . actionsBeingParsed != nil {
ret . actionsBeingParsed = append ( ret . actionsBeingParsed , g . actionsBeingParsed ... )
}
if g . actions != nil {
2020-06-26 11:00:14 -04:00
ret . actions = make ( map [ action . Type ] bool , len ( g . actions ))
2020-06-10 17:58:52 -04:00
for action := range g . actions {
ret . actions [ action ] = true
}
}
2023-01-06 17:27:19 -06:00
if outFields , hasSetFields := g . OutputFields . Fields (); hasSetFields {
fieldsToAdd := make ([] string , 0 , len ( outFields ))
2024-12-06 16:45:38 -05:00
fieldsToAdd = append ( fieldsToAdd , outFields ... )
2023-01-06 17:27:19 -06:00
ret . OutputFields = ret . OutputFields . AddFields ( fieldsToAdd )
2021-05-04 19:57:35 -05:00
}
2020-06-10 17:58:52 -04:00
return ret
}
// CanonicalString returns the canonical representation of the grant
func ( g Grant ) CanonicalString () string {
var builder [] string
if g . id != "" {
builder = append ( builder , fmt . Sprintf ( "id=%s" , g . id ))
}
2023-06-06 16:13:44 -04:00
if len ( g . ids ) > 0 {
builder = append ( builder , fmt . Sprintf ( "ids=%s" , strings . Join ( g . ids , "," )))
}
2020-06-26 11:00:14 -04:00
if g . typ != resource . Unknown {
builder = append ( builder , fmt . Sprintf ( "type=%s" , g . typ . String ()))
2020-06-10 17:58:52 -04:00
}
if len ( g . actions ) > 0 {
actions := make ([] string , 0 , len ( g . actions ))
for action := range g . actions {
actions = append ( actions , action . String ())
}
sort . Strings ( actions )
builder = append ( builder , fmt . Sprintf ( "actions=%s" , strings . Join ( actions , "," )))
}
2023-01-06 17:27:19 -06:00
if outFields , hasSetFields := g . OutputFields . Fields (); hasSetFields {
builder = append ( builder , fmt . Sprintf ( "output_fields=%s" , strings . Join ( outFields , "," )))
2021-05-04 19:57:35 -05:00
}
2020-06-10 17:58:52 -04:00
return strings . Join ( builder , ";" )
}
// MarshalJSON provides a custom marshaller for grants
2024-02-21 16:32:26 -05:00
func ( g Grant ) MarshalJSON () ([] byte , error ) {
2021-01-13 15:50:07 -08:00
const op = "perms.(Grant).MarshalJSON"
2022-10-25 18:29:38 -05:00
res := make ( map [ string ] any , 4 )
2020-06-10 17:58:52 -04:00
if g . id != "" {
res [ "id" ] = g . id
}
2023-06-06 16:13:44 -04:00
if len ( g . ids ) > 0 {
res [ "ids" ] = g . ids
}
2020-06-26 11:00:14 -04:00
if g . typ != resource . Unknown {
res [ "type" ] = g . typ . String ()
2020-06-10 17:58:52 -04:00
}
if len ( g . actions ) > 0 {
actions := make ([] string , 0 , len ( g . actions ))
for action := range g . actions {
actions = append ( actions , action . String ())
}
sort . Strings ( actions )
res [ "actions" ] = actions
}
2023-01-06 17:27:19 -06:00
if outFields , hasSetFields := g . OutputFields . Fields (); hasSetFields {
res [ "output_fields" ] = outFields
2021-05-04 19:57:35 -05:00
}
2021-01-13 15:50:07 -08:00
b , err := json . Marshal ( res )
if err != nil {
2024-02-21 16:32:26 -05:00
return nil , fmt . Errorf ( "%s: error marshaling grant: %w" , op , err )
2021-01-13 15:50:07 -08:00
}
return b , nil
2020-06-10 17:58:52 -04:00
}
// This is purposefully unexported since the values being set here are not being
// checked for validity. This should only be called by the main parsing function
// when JSON is detected.
2023-06-06 17:09:08 -04:00
func ( g * Grant ) unmarshalJSON ( ctx context . Context , data [] byte ) error {
2021-01-13 15:50:07 -08:00
const op = "perms.(Grant).unmarshalJSON"
2022-10-25 18:29:38 -05:00
raw := make ( map [ string ] any , 4 )
2020-06-10 17:58:52 -04:00
if err := json . Unmarshal ( data , & raw ); err != nil {
2023-06-06 17:09:08 -04:00
return errors . Wrap ( ctx , err , op , errors . WithCode ( errors . Decode ))
2020-06-10 17:58:52 -04:00
}
if rawId , ok := raw [ "id" ]; ok {
id , ok := rawId .( string )
2023-09-06 09:11:56 -07:00
switch {
case ! ok :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q as string" , "id" ))
2023-09-06 09:11:56 -07:00
case id == "" :
return errors . New ( ctx , errors . InvalidParameter , op , "empty ID provided" )
case strings . ContainsAny ( id , ",;=" ):
return errors . New ( ctx , errors . InvalidParameter , op , "ID cannot contain a comma, semicolon or equals sign" )
2020-06-10 17:58:52 -04:00
}
2020-12-18 13:25:28 -05:00
g . id = id
2020-06-10 17:58:52 -04:00
}
2023-06-06 16:13:44 -04:00
if rawIds , ok := raw [ "ids" ]; ok {
ids , ok := rawIds .([] any )
if ! ok {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q as array" , "ids" ))
2023-06-06 16:13:44 -04:00
}
g . ids = make ([] string , len ( ids ))
for i , id := range ids {
idStr , ok := id .( string )
2023-09-06 09:11:56 -07:00
switch {
case ! ok :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q element %q as string" , "ids" , id ))
2023-09-06 09:11:56 -07:00
case idStr == "" :
return errors . New ( ctx , errors . InvalidParameter , op , "empty ID provided" )
case strings . ContainsAny ( idStr , ",;=" ):
return errors . New ( ctx , errors . InvalidParameter , op , "ID cannot contain a comma, semicolon or equals sign" )
2023-06-06 16:13:44 -04:00
}
g . ids [ i ] = idStr
}
}
2020-06-10 17:58:52 -04:00
if rawType , ok := raw [ "type" ]; ok {
typ , ok := rawType .( string )
if ! ok {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q as string" , "type" ))
2020-06-10 17:58:52 -04:00
}
2020-07-14 14:32:47 -07:00
g . typ = resource . Map [ typ ]
if g . typ == resource . Unknown {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown type specifier %q" , typ ))
2020-07-14 14:32:47 -07:00
}
2020-06-10 17:58:52 -04:00
}
if rawActions , ok := raw [ "actions" ]; ok {
2022-10-25 18:29:38 -05:00
interfaceActions , ok := rawActions .([] any )
2020-06-10 17:58:52 -04:00
if ! ok {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q as array" , "actions" ))
2020-06-10 17:58:52 -04:00
}
if len ( interfaceActions ) > 0 {
g . actionsBeingParsed = make ([] string , 0 , len ( interfaceActions ))
for _ , v := range interfaceActions {
actionStr , ok := v .( string )
switch {
case ! ok :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %v in actions array as string" , v ))
2020-06-10 17:58:52 -04:00
case actionStr == "" :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "empty action found" )
2023-09-06 09:11:56 -07:00
case strings . ContainsAny ( actionStr , ",;=" ):
return errors . New ( ctx , errors . InvalidParameter , op , "action cannot contain a comma, semicolon or equals sign" )
2020-06-10 17:58:52 -04:00
default :
g . actionsBeingParsed = append ( g . actionsBeingParsed , strings . ToLower ( actionStr ))
}
}
}
}
2021-05-04 19:57:35 -05:00
if rawOutputFields , ok := raw [ "output_fields" ]; ok {
2022-10-25 18:29:38 -05:00
interfaceOutputFields , ok := rawOutputFields .([] any )
2021-05-04 19:57:35 -05:00
if ! ok {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %q as array" , "output_fields" ))
2021-05-04 19:57:35 -05:00
}
// We do the make here because we detect later if the field was set but
// no values given
2023-01-06 17:27:19 -06:00
switch len ( interfaceOutputFields ) {
case 0 :
// JSON was set but no fields defined, add an empty array
g . OutputFields = g . OutputFields . AddFields ([] string {})
default :
fields := make ([] string , 0 , len ( interfaceOutputFields ))
2021-05-04 19:57:35 -05:00
for _ , v := range interfaceOutputFields {
field , ok := v .( string )
switch {
case ! ok :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unable to interpret %v in output_fields array as string" , v ))
2023-09-06 09:11:56 -07:00
case strings . ContainsAny ( field , ",;=" ):
return errors . New ( ctx , errors . InvalidParameter , op , "output fields cannot contain a comma, semicolon or equals sign" )
2021-05-04 19:57:35 -05:00
default :
2023-01-06 17:27:19 -06:00
fields = append ( fields , field )
2021-05-04 19:57:35 -05:00
}
}
2023-01-06 17:27:19 -06:00
g . OutputFields = g . OutputFields . AddFields ( fields )
2021-05-04 19:57:35 -05:00
}
}
2020-06-10 17:58:52 -04:00
return nil
}
2023-06-06 17:09:08 -04:00
func ( g * Grant ) unmarshalText ( ctx context . Context , grantString string ) error {
2021-01-13 15:50:07 -08:00
const op = "perms.(Grant).unmarshalText"
2020-06-10 17:58:52 -04:00
segments := strings . Split ( grantString , ";" )
for _ , segment := range segments {
kv := strings . Split ( segment , "=" )
// Ensure we don't accept "foo=bar=baz", "=foo", or "foo="
switch {
case len ( kv ) != 2 :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "segment %q not formatted correctly, wrong number of equal signs" , segment ))
2020-06-10 17:58:52 -04:00
case len ( kv [ 0 ]) == 0 :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "segment %q not formatted correctly, missing key" , segment ))
2023-01-06 17:27:19 -06:00
case len ( kv [ 1 ]) == 0 && kv [ 0 ] != "output_fields" :
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "segment %q not formatted correctly, missing value" , segment ))
2020-06-10 17:58:52 -04:00
}
switch kv [ 0 ] {
case "id" :
2020-12-18 13:25:28 -05:00
g . id = kv [ 1 ]
2023-09-06 09:11:56 -07:00
if strings . Contains ( g . id , "," ) {
return errors . New ( ctx , errors . InvalidParameter , op , "ID cannot contain a comma" )
}
2020-06-10 17:58:52 -04:00
2023-06-06 16:13:44 -04:00
case "ids" :
g . ids = strings . Split ( kv [ 1 ], "," )
2023-09-06 09:11:56 -07:00
for _ , id := range g . ids {
if id == "" {
return errors . New ( ctx , errors . InvalidParameter , op , "empty ID provided" )
}
}
2023-06-06 16:13:44 -04:00
2020-06-10 17:58:52 -04:00
case "type" :
2020-06-26 11:00:14 -04:00
typeString := strings . ToLower ( kv [ 1 ])
2020-07-14 14:32:47 -07:00
g . typ = resource . Map [ typeString ]
2020-06-26 11:00:14 -04:00
if g . typ == resource . Unknown {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown type specifier %q" , typeString ))
2020-06-26 11:00:14 -04:00
}
2020-06-10 17:58:52 -04:00
case "actions" :
actions := strings . Split ( kv [ 1 ], "," )
if len ( actions ) > 0 {
g . actionsBeingParsed = make ([] string , 0 , len ( actions ))
for _ , action := range actions {
if action == "" {
2023-06-06 17:09:08 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "empty action found" )
2020-06-10 17:58:52 -04:00
}
g . actionsBeingParsed = append ( g . actionsBeingParsed , strings . ToLower ( action ))
}
}
2021-05-04 19:57:35 -05:00
case "output_fields" :
2023-01-06 17:27:19 -06:00
switch len ( kv [ 1 ]) {
case 0 :
g . OutputFields = g . OutputFields . AddFields ([] string {})
default :
g . OutputFields = g . OutputFields . AddFields ( strings . Split ( kv [ 1 ], "," ))
}
2020-06-10 17:58:52 -04:00
}
}
return nil
}
// Parse parses a grant string. Note that this does not do checking
// of the validity of IDs and such; that's left for other parts of the system.
// We may not check at all (e.g. let it be an authz-time failure) or could check
// after submission to catch errors.
//
// The scope must be the org and project where this grant originated, not the
// request.
2024-09-17 10:07:43 -04:00
func Parse ( ctx context . Context , tuple GrantTuple , opt ... Option ) ( Grant , error ) {
2021-01-13 15:50:07 -08:00
const op = "perms.Parse"
2024-09-17 10:07:43 -04:00
if len ( tuple . Grant ) == 0 {
2023-06-06 16:13:44 -04:00
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , "missing grant string" )
2020-06-10 17:58:52 -04:00
}
2024-09-17 10:07:43 -04:00
if tuple . RoleScopeId == "" {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , "missing role scope id" )
}
if tuple . GrantScopeId == "" {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , "missing grant scope id" )
2020-06-10 17:58:52 -04:00
}
2024-09-17 10:07:43 -04:00
tuple . Grant = strings . ToValidUTF8 ( tuple . Grant , string ( unicode . ReplacementChar ))
2020-06-10 17:58:52 -04:00
grant := Grant {
2024-09-17 10:07:43 -04:00
roleScopeId : strings . ToValidUTF8 ( tuple . RoleScopeId , string ( unicode . ReplacementChar )),
roleParentScopeId : tuple . RoleParentScopeId ,
grantScopeId : tuple . GrantScopeId ,
2020-06-10 17:58:52 -04:00
}
switch {
2024-09-17 10:07:43 -04:00
case tuple . Grant [ 0 ] == '{' :
if err := grant . unmarshalJSON ( ctx , [] byte ( tuple . Grant )); err != nil {
2023-06-06 16:13:44 -04:00
return Grant {}, errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to parse JSON grant string" ))
2020-06-10 17:58:52 -04:00
}
default :
2024-09-17 10:07:43 -04:00
if err := grant . unmarshalText ( ctx , tuple . Grant ); err != nil {
2023-06-06 16:13:44 -04:00
return Grant {}, errors . Wrap ( ctx , err , op , errors . WithMsg ( "unable to parse grant string" ))
2020-06-10 17:58:52 -04:00
}
}
2023-06-06 16:13:44 -04:00
if grant . id != "" && len ( grant . ids ) > 0 {
2024-09-17 10:07:43 -04:00
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "input grant string %q contains both %q and %q fields" , tuple . Grant , "id" , "ids" ))
2020-06-10 17:58:52 -04:00
}
2023-06-06 16:13:44 -04:00
if len ( grant . ids ) > 1 && slices . Contains ( grant . ids , "*" ) {
2024-09-17 10:07:43 -04:00
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "input grant string %q contains both wildcard and non-wildcard values in %q field" , tuple . Grant , "ids" ))
2020-06-10 17:58:52 -04:00
}
2023-06-06 16:13:44 -04:00
opts := getOpts ( opt ... )
2020-06-10 17:58:52 -04:00
2023-06-06 16:13:44 -04:00
var grantIds [] string
var deprecatedId bool
switch {
case grant . id != "" :
grantIds = [] string { grant . id }
deprecatedId = true
case len ( grant . ids ) > 0 :
grantIds = grant . ids
// Ensure we aren't seeing mixed types. We will have already filtered
// out the wildcard case above.
if len ( grant . ids ) > 1 {
var seenType resource . Type
for i , id := range grantIds {
if i == 0 {
2023-11-15 19:55:46 -05:00
seenType = globals . ResourceInfoFromPrefix ( id ). Type
2023-06-06 16:13:44 -04:00
continue
2023-03-16 10:11:55 -04:00
}
2023-11-15 19:55:46 -05:00
if seenType != globals . ResourceInfoFromPrefix ( id ). Type {
2024-09-17 10:07:43 -04:00
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "input grant string %q contains ids of differently-typed resources" , tuple . Grant ))
2023-03-16 10:11:55 -04:00
}
}
2023-06-06 16:13:44 -04:00
}
}
// It's possible that there is no id in a grant. In that case we still need
// to validate it and build up the parsed Grant. We insert an empty ID value
// in this case; the code below will check that it's non-empty before
// running any ID-specific logic on it.
if len ( grantIds ) == 0 {
grantIds = [] string { "" }
}
for i , currId := range grantIds {
// Check for templated values ID, and substitute in with the authenticated
// values if so. If we are using a dummy user or account ID, store the
// original ID and return it at the end; this is usually the case when
// validating grant formats.
var origId string
if currId != "" {
if strings . HasPrefix ( currId , "{{" ) {
id := strings . TrimSuffix ( strings . TrimPrefix ( currId , "{{" ), "}}" )
id = strings . TrimSpace ( id )
switch id {
case "user.id" , ".User.Id" :
if opts . withUserId != "" {
grantIds [ i ] = strings . ToValidUTF8 ( opts . withUserId , string ( unicode . ReplacementChar ))
} else {
// Otherwise, substitute in a dummy value
origId = currId
grantIds [ i ] = "u_dummy"
2021-02-05 12:39:55 -05:00
}
2023-06-06 16:13:44 -04:00
case "account.id" , ".Account.Id" :
if opts . withAccountId != "" {
grantIds [ i ] = strings . ToValidUTF8 ( opts . withAccountId , string ( unicode . ReplacementChar ))
} else {
origId = currId
grantIds [ i ] = "acctoidc_dummy"
2021-02-05 12:39:55 -05:00
}
default :
2023-06-06 16:13:44 -04:00
fieldName := "ids"
if deprecatedId {
fieldName = "id"
}
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown template %q in grant %q value" , currId , fieldName ))
2021-02-05 12:39:55 -05:00
}
}
}
2023-06-06 16:13:44 -04:00
// We don't need to do these twice as they don't depend on IDs; they
// also clear state such as actionsBeingParsed
if i == 0 {
if err := grant . validateType ( ctx ); err != nil {
return Grant {}, errors . Wrap ( ctx , err , op )
2021-05-04 19:57:35 -05:00
}
2023-06-06 16:13:44 -04:00
if err := grant . parseAndValidateActions ( ctx ); err != nil {
return Grant {}, errors . Wrap ( ctx , err , op )
2021-05-04 19:57:35 -05:00
}
2023-06-06 16:13:44 -04:00
}
if ! opts . withSkipFinalValidation {
switch {
case grantIds [ i ] == "*" :
// Matches
// id=*;type=sometype;actions=foo,bar
// or
// id=*;type=*;actions=foo,bar
// This can be a non-unknown type or wildcard
if grant . typ == resource . Unknown {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains wildcard id and no specified type" , grant . CanonicalString ()))
}
case grantIds [ i ] != "" :
// Non-wildcard but specified ID. This can match
// id=foo_bar;actions=foo,bar
// or
// id=foo_bar;type=sometype;actions=foo,bar
// or
// id=foo_bar;type=*;actions=foo,bar
// but notably the specified types have to actually make sense: in
// the second example the type corresponding to the ID must have the
// specified type as a child type; in the third the ID must be a
// type that has child types.
2023-11-15 19:55:46 -05:00
idType := globals . ResourceInfoFromPrefix ( grantIds [ i ]). Type
2023-06-06 16:13:44 -04:00
if idType == resource . Unknown {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains an id %q of an unknown resource type" , grant . CanonicalString (), grantIds [ i ]))
}
switch grant . typ {
case resource . Unknown :
// This is fine as-is but we do not support collection actions
// without a type (either directly specified or wildcard) so
// check that
if grant . actions [ action . Create ] ||
grant . actions [ action . List ] {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains create or list action in a format that does not allow these" , grant . CanonicalString ()))
}
case resource . All :
// Verify that the ID is a type that has child types
if ! resource . HasChildTypes ( idType ) {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains an id that does not support child types" , grant . CanonicalString ()))
}
default :
// Specified resource type, verify it's a child
if resource . Parent ( grant . typ ) != idType {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains type %s that is not a child type of the type (%s) of the specified id" , grant . CanonicalString (), grant . typ . String (), idType . String ()))
}
}
default : // no specified id
switch grant . typ {
case resource . Unknown :
// Error -- no ID or type isn't valid (although we should never
// get to this point because original parsing should error)
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains no id or type" , grant . CanonicalString ()))
case resource . All :
// "type=*;actions=..." is not supported -- we require you to
// explicitly set a pin or set the ID to *
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains wildcard type with no id value" , grant . CanonicalString ()))
default :
// Here we have type=something,actions=<something else>. This
// means we're operating on collections and support only create
// or list. Note that wildcard actions are not okay here; that
// uses the format id=*;type=<something>;actions=*
switch len ( grant . actions ) {
case 0 :
// It's okay to have no actions if only output fields are being defined
if _ , hasSetFields := grant . OutputFields . Fields (); ! hasSetFields {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains no actions or output fields" , grant . CanonicalString ()))
}
case 1 :
if ! grant . hasActionOrSubaction ( action . Create ) &&
! grant . hasActionOrSubaction ( action . List ) {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains non-create or non-list action in a format that only allows these" , grant . CanonicalString ()))
}
case 2 :
if ! grant . hasActionOrSubaction ( action . Create ) || ! grant . hasActionOrSubaction ( action . List ) {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains non-create or non-list action in a format that only allows these" , grant . CanonicalString ()))
}
default :
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q contains non-create or non-list action in a format that only allows these" , grant . CanonicalString ()))
}
2021-05-04 19:57:35 -05:00
}
}
2023-06-06 16:13:44 -04:00
// This might be zero if output fields is populated
if len ( grant . actions ) > 0 {
// Create a dummy resource and pass it through Allowed and
// ensure that we get allowed. We need to use the templated
// grant, if any, so we send in a clone with an updated ID.
grantForValidation := grant . clone ()
grantForValidation . id = grantIds [ i ]
acl := NewACL ( * grantForValidation )
2024-09-17 10:07:43 -04:00
// For special scope names we aren't sure where the resource
// might be, so check possible scopes and see if any are valid
scopesToCheck := make ([] string , 0 , 2 )
var parentScopeId string
switch {
case grant . grantScopeId == globals . GrantScopeDescendants :
scopesToCheck = append ( scopesToCheck , "o_1234567890" , "p_1234567890" )
case grant . grantScopeId == globals . GrantScopeChildren :
if grant . roleScopeId == scope . Global . String () {
scopesToCheck = append ( scopesToCheck , "o_1234567890" )
parentScopeId = scope . Global . String ()
} else {
scopesToCheck = append ( scopesToCheck , "p_1234567890" )
parentScopeId = grant . roleScopeId
}
default :
scopesToCheck = append ( scopesToCheck , grant . grantScopeId )
2023-06-06 16:13:44 -04:00
}
var allowed bool
2024-09-17 10:07:43 -04:00
for _ , scopeId := range scopesToCheck {
r := Resource {
ScopeId : scopeId ,
Id : grantIds [ i ],
Type : grant . typ ,
ParentScopeId : parentScopeId ,
}
if ! resource . TopLevelType ( grant . typ ) {
r . Pin = grantIds [ i ]
}
for k := range grant . actions {
results := acl . Allowed ( r , k , globals . AnonymousUserId , WithSkipAnonymousUserRestrictions ( true ))
if results . Authorized {
allowed = true
break
}
}
if allowed {
2023-06-06 16:13:44 -04:00
break
}
}
if ! allowed {
return Grant {}, errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "parsed grant string %q would not result in any action being authorized" , grant . CanonicalString ()))
}
2020-10-01 01:49:20 -04:00
}
}
2023-06-06 16:13:44 -04:00
// If we substituted in a dummy value, replace with the original now
if origId != "" {
grantIds [ i ] = origId
}
2020-10-01 01:49:20 -04:00
}
2023-06-06 16:13:44 -04:00
// See if we need to move grantIds back for the deprecated case. grantIds
// will always be at least size 1 since we add the empty string if no IDs
// were provided, so we can check to see if that was the case first.
switch {
case grantIds [ 0 ] == "" :
// Nothing to do
case deprecatedId :
grant . id = grantIds [ 0 ]
default :
grant . ids = grantIds
2023-03-16 10:11:55 -04:00
}
2020-06-10 17:58:52 -04:00
return grant , nil
}
2021-09-13 13:56:44 -04:00
// validateType ensures that we are not allowing access to disallowed resource
// types. It does not explicitly check the resource string itself; that's the
// job of the parsing functions to look up the string from the Map and ensure
// it's not unknown.
2023-06-06 16:13:44 -04:00
func ( g Grant ) validateType ( ctx context . Context ) error {
2021-01-13 15:50:07 -08:00
const op = "perms.(Grant).validateType"
2020-06-10 17:58:52 -04:00
switch g . typ {
2022-06-05 16:43:02 -04:00
case resource . Controller :
2023-06-06 16:13:44 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown type specifier %q" , g . typ ))
2020-06-10 17:58:52 -04:00
}
2021-09-13 13:56:44 -04:00
return nil
2020-06-10 17:58:52 -04:00
}
2023-06-06 16:13:44 -04:00
func ( g * Grant ) parseAndValidateActions ( ctx context . Context ) error {
2021-01-13 15:50:07 -08:00
const op = "perms.(Grant).parseAndValidateActions"
2020-06-10 17:58:52 -04:00
if len ( g . actionsBeingParsed ) == 0 {
2021-05-04 19:57:35 -05:00
g . actionsBeingParsed = nil
// If there are no actions it's fine if the grant is just used to
// specify output fields
2023-01-06 17:27:19 -06:00
if _ , hasSetFields := g . OutputFields . Fields (); hasSetFields {
2021-05-04 19:57:35 -05:00
return nil
}
2023-06-06 16:13:44 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "missing actions" )
2020-06-10 17:58:52 -04:00
}
2020-06-26 11:00:14 -04:00
for _ , a := range g . actionsBeingParsed {
if a == "" {
2023-06-06 16:13:44 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , "empty action found" )
2020-06-10 17:58:52 -04:00
}
if g . actions == nil {
2020-06-26 11:00:14 -04:00
g . actions = make ( map [ action . Type ] bool , len ( g . actionsBeingParsed ))
2020-06-10 17:58:52 -04:00
}
2020-06-26 11:00:14 -04:00
if am := action . Map [ a ]; am == action . Unknown {
2023-06-06 16:13:44 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "unknown action %q" , a ))
2020-06-10 17:58:52 -04:00
} else {
2020-06-26 11:00:14 -04:00
g . actions [ am ] = true
2020-06-10 17:58:52 -04:00
}
}
2020-06-26 11:00:14 -04:00
if len ( g . actions ) > 1 && g . actions [ action . All ] {
2023-06-06 16:13:44 -04:00
return errors . New ( ctx , errors . InvalidParameter , op , fmt . Sprintf ( "%q cannot be specified with other actions" , action . All . String ()))
2020-06-10 17:58:52 -04:00
}
g . actionsBeingParsed = nil
return nil
}