Skip to content

Commit

Permalink
Issue #57, New parser for hexa policy values and detect entity relati…
Browse files Browse the repository at this point in the history
…onships.
  • Loading branch information
independentid committed Oct 14, 2024
1 parent 410201c commit 680781d
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 0 deletions.
45 changes: 45 additions & 0 deletions pkg/hexapolicy/parser/EntityValueFormat.md
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:alice@example.com"]` | `principal == User::"alice@example.com"` |
| 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"]` |
168 changes: 168 additions & 0 deletions pkg/hexapolicy/parser/entity.go
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:alice@example.co`
)

// 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]
}
118 changes: 118 additions & 0 deletions pkg/hexapolicy/parser/entity_test.go
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")
})
}
}

0 comments on commit 680781d

Please sign in to comment.