-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue #57, New parser for hexa policy values and detect entity relati…
…onships.
- Loading branch information
1 parent
410201c
commit 680781d
Showing
3 changed files
with
331 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Formats for Hexa IDQL Policy Entity Values | ||
|
||
These variations are used to indicate different matching scenarios for entities | ||
within IDQL policy. An entity is formatted value passed in for subjects, actions, or object. | ||
|
||
## Any or AnyAuthenticated | ||
|
||
Used for subjects values, the special purpose value of `any` (as in anything), or `anyAuthenticated` (an identified subject or User) may be used. | ||
|
||
## Equality | ||
|
||
Indicates a subject is an identified type with an identifier | ||
|
||
`<type>:<id>` example `Subjects = ["User:alicesmith"]` | ||
|
||
Cedar: principle == User::"alicesmith" | ||
|
||
## Type Is | ||
|
||
Indicates a type of subject | ||
|
||
`<type>:` example: `User:` | ||
|
||
## Type Is In | ||
|
||
Express that a type of entity within a group | ||
|
||
`<type>[<entity>]` example: `User[Group:administrators]` | ||
|
||
## In Relationship | ||
Express that the item falls with a group | ||
|
||
`[<entity>]` example: `[Group:administrators]` | ||
|
||
For objects: | ||
`[<entity>,...]` example: `[Photo:mypic1.jpg,Photo:mypic2.jpg]` | ||
|
||
Note: Because IDQL allows multiples subjects and actions, there is no need for a set unless | ||
|
||
| Comparison | Syntax | IDQL | Cedar | | ||
|------------|------------------------|------------------------------------------------|------------------------------------------------------| | ||
| Equality | `<type>:<id>` | `subjects = ["User:[email protected]"]` | `principal == User::"[email protected]"` | | ||
| Is type | `<type>:` | `subjects = ["User:"]` | `principal is User` | | ||
| Is In | `<type>[<entity>,...]` | `subjects = ["User[Group:Admins]"]` | `principal is User in Group::"Admins"` | | ||
| In | `[<entity>,...]` | `subjects = [ "[Group:Admins,Group:Employees]` | `principal in [Group::"Admins", Group::"Employees"]` | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
// Package parser is used to parse values that represent entities that are contained within IDQL | ||
// `PolicyInfo` for `SubjectInfo`, `ActionInfo`, and `Object`. This package will | ||
// be used by the schema validator to evaluate whether an IDQL policy conforms to policy. | ||
package parser | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
const ( | ||
RelTypeAny = "any" // Used for allowing any subject including anonymous | ||
RelTypeAnyAuthenticated = "anyAuthenticated" // Used for allowing any subject that was authenticated | ||
RelTypeIs = "is" // Matching by type such as `User:` | ||
RelTypeIsIn = "isIn" // Type is in set such as `User[Group:admins]` | ||
RelTypeIn = "in" // Matching through membership in a set or entity [Group:admins] | ||
RelTypeEquals = "eq" // Matches a specific type and identifier e.g. `User:[email protected]` | ||
) | ||
|
||
// EntityPath represents a path that points to an entity used in IDQL policy (Subjects, Actions, Object). | ||
type EntityPath struct { | ||
Types []string // Types is the parsed entity structure e.g. PhotoApp:Photo | ||
Type string // The type of relationship being expressed (see RelTypeEquals, ...) | ||
Id *string // The id of a specific entity instance within type. (e.g. myvactionphoto.jpg) | ||
In *[]EntityPath // When an entity represents a set of entities (e.g. [PhotoApp:Photo:picture1.jpg,PhotoApp:Photo:picture2.jpg]) | ||
// *string | ||
} | ||
|
||
// ParseEntityPath takes a string value from an IDQL Subject, Action, or Object parses | ||
// it into an EntityPath struct. | ||
func ParseEntityPath(value string) *EntityPath { | ||
var typePath []string | ||
var sets []EntityPath | ||
var id *string | ||
|
||
sb := strings.Builder{} | ||
setb := strings.Builder{} | ||
isSet := false | ||
for _, r := range value { | ||
if r == ':' && !isSet { | ||
// found entity separator | ||
typePath = append(typePath, sb.String()) | ||
sb.Reset() | ||
continue | ||
} | ||
if r == '[' { | ||
// found set | ||
isSet = true | ||
if sb.Len() > 0 { | ||
// save any parsed type before parsing set | ||
typePath = append(typePath, sb.String()) | ||
sb.Reset() | ||
} | ||
continue | ||
} | ||
if r == ']' { | ||
isSet = false | ||
setString := setb.String() | ||
inset := strings.Split(setString, ",") | ||
for _, member := range inset { | ||
entitypath := ParseEntityPath(member) | ||
if entitypath != nil { | ||
sets = append(sets, *entitypath) | ||
} | ||
} | ||
setb.Reset() | ||
continue | ||
} | ||
|
||
if isSet { | ||
setb.WriteRune(r) | ||
} else { | ||
sb.WriteRune(r) | ||
} | ||
} | ||
if sb.Len() > 0 { | ||
idValue := sb.String() | ||
id = &idValue | ||
} | ||
|
||
if id != nil { | ||
if strings.EqualFold(*id, "any") { | ||
return &EntityPath{ | ||
Types: nil, | ||
Type: RelTypeAny, | ||
} | ||
} | ||
if strings.EqualFold(*id, "anyauthenticated") { | ||
return &EntityPath{ | ||
Types: nil, | ||
Type: RelTypeAnyAuthenticated, | ||
} | ||
} | ||
} | ||
|
||
if sets != nil && len(sets) > 0 { | ||
// This is an in or is in | ||
if typePath == nil || len(typePath) == 0 { | ||
// this is an in | ||
return &EntityPath{ | ||
Types: nil, | ||
Type: RelTypeIn, | ||
In: &sets, | ||
} | ||
} | ||
return &EntityPath{ | ||
Type: RelTypeIsIn, | ||
Types: typePath, | ||
In: &sets, | ||
} | ||
} | ||
|
||
// This is an is (e.g. User:) | ||
if id == nil { | ||
return &EntityPath{ | ||
Type: RelTypeIs, | ||
Types: typePath, | ||
} | ||
} | ||
|
||
// This is just a straight equals (e.g. User:alice) | ||
return &EntityPath{ | ||
Type: RelTypeEquals, | ||
Types: typePath, | ||
Id: id, | ||
} | ||
} | ||
|
||
func (e *EntityPath) String() string { | ||
switch e.Type { | ||
case RelTypeAny: | ||
return "any" | ||
case RelTypeAnyAuthenticated: | ||
return "anyAuthenticated" | ||
case RelTypeEquals: | ||
return fmt.Sprintf("%s:%s", strings.Join(e.Types, ":"), *e.Id) | ||
case RelTypeIs: | ||
return fmt.Sprintf("%s:", strings.Join(e.Types, ":")) | ||
case RelTypeIsIn: | ||
sb := strings.Builder{} | ||
for i, entity := range *e.In { | ||
if i > 0 { | ||
sb.WriteString(",") | ||
} | ||
sb.WriteString(entity.String()) | ||
} | ||
return fmt.Sprintf("%s[%s]", strings.Join(e.Types, ":"), sb.String()) | ||
case RelTypeIn: | ||
sb := strings.Builder{} | ||
for i, entity := range *e.In { | ||
if i > 0 { | ||
sb.WriteString(",") | ||
} | ||
sb.WriteString(entity.String()) | ||
} | ||
return fmt.Sprintf("[%s]", sb.String()) | ||
} | ||
return "unexpected type: " + e.Type | ||
} | ||
|
||
// GetType returns the immediate parent type. For exmaple: for PhotoApp:User:smith, the type is User | ||
// If no parent is defined an empty string "" is returned | ||
func (e *EntityPath) GetType() string { | ||
if len(e.Types) == 0 { | ||
return "" | ||
} | ||
return e.Types[len(e.Types)-1] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package parser | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestEntityPath(t *testing.T) { | ||
idval := "gerry" | ||
idgroup := "admins" | ||
inEntity := EntityPath{Type: RelTypeEquals, Types: []string{"Group"}, Id: &idgroup} | ||
inEntity2 := EntityPath{Type: RelTypeEquals, Types: []string{"Employee"}, Id: &idgroup} | ||
inEntities := []EntityPath{inEntity} | ||
inEntitiesMulti := []EntityPath{inEntity, inEntity2} | ||
getPhoto := "getPhoto" | ||
tests := []struct { | ||
name string | ||
input string | ||
want EntityPath | ||
wantType string | ||
}{ | ||
{ | ||
name: "Any", | ||
input: "any", | ||
want: EntityPath{ | ||
Type: RelTypeAny, | ||
}, | ||
wantType: "", | ||
}, | ||
{ | ||
name: "Authenticated", | ||
input: "anyAuthenticated", | ||
want: EntityPath{ | ||
Type: RelTypeAnyAuthenticated, | ||
}, | ||
wantType: "", | ||
}, | ||
{ | ||
name: "Is User", | ||
input: "User:", | ||
want: EntityPath{ | ||
Type: RelTypeIs, | ||
Types: []string{"User"}, | ||
}, | ||
wantType: "User", | ||
}, | ||
{ | ||
name: "User:gerry", | ||
input: "User:gerry", | ||
want: EntityPath{ | ||
Type: RelTypeEquals, | ||
Types: []string{"User"}, | ||
Id: &idval, | ||
}, | ||
wantType: "User", | ||
}, | ||
{ | ||
name: "Multi-entity type", | ||
input: "PhotoApp:Action:getPhoto", | ||
want: EntityPath{ | ||
Type: RelTypeEquals, | ||
Types: []string{"PhotoApp", "Action"}, | ||
Id: &getPhoto, | ||
}, | ||
wantType: "Action", | ||
}, | ||
{ | ||
name: "Is User in Group", | ||
input: "User[Group:admins]", | ||
want: EntityPath{ | ||
Type: RelTypeIsIn, | ||
Types: []string{"User"}, | ||
Id: nil, | ||
In: &inEntities, | ||
}, | ||
wantType: "User", | ||
}, | ||
{ | ||
name: "In Group", | ||
input: "[Group:admins]", | ||
want: EntityPath{ | ||
Type: RelTypeIn, | ||
Id: nil, | ||
In: &inEntities, | ||
}, | ||
wantType: "", | ||
}, | ||
{ | ||
name: "In set of entities", | ||
input: "[Group:admins,Employee:admins]", | ||
want: EntityPath{ | ||
Type: RelTypeIn, | ||
Id: nil, | ||
In: &inEntitiesMulti, | ||
}, | ||
wantType: "", | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
fmt.Println("Testing: " + tt.name) | ||
|
||
result := ParseEntityPath(tt.input) | ||
assert.NotNil(t, result) | ||
if !reflect.DeepEqual(*result, tt.want) { | ||
} | ||
|
||
// Test that the String() function is working | ||
assert.Equal(t, tt.input, result.String(), "String() should produce original input") | ||
|
||
assert.Equal(t, tt.wantType, result.GetType(), "Object type should match") | ||
}) | ||
} | ||
} |