From c0a9763b0909a0ae6d6b4572c8c6e062cef1da36 Mon Sep 17 00:00:00 2001 From: Karan Kajla Date: Sun, 31 Mar 2024 13:17:49 -0700 Subject: [PATCH] Add policy support to query endpoint (#315) --- pkg/authz/query/handlers.go | 20 +- pkg/authz/query/parser.go | 1 - pkg/authz/query/resultset.go | 44 +- pkg/authz/query/service.go | 156 +++- pkg/authz/query/spec.go | 32 +- tests/v2/query-policy.json | 1370 ++++++++++++++++++++++++++++++++++ 6 files changed, 1553 insertions(+), 70 deletions(-) create mode 100644 tests/v2/query-policy.json diff --git a/pkg/authz/query/handlers.go b/pkg/authz/query/handlers.go index 54a6ef1..be02aa7 100644 --- a/pkg/authz/query/handlers.go +++ b/pkg/authz/query/handlers.go @@ -42,12 +42,20 @@ func (svc QueryService) Routes() ([]service.Route, error) { } func queryV1(svc QueryService, w http.ResponseWriter, r *http.Request) error { - queryString := r.URL.Query().Get("q") + queryParams := r.URL.Query() + queryString := queryParams.Get("q") query, err := NewQueryFromString(queryString) if err != nil { return err } + if queryParams.Has("context") { + err = query.WithContext(queryParams.Get("context")) + if err != nil { + return service.NewInvalidParameterError("context", "invalid") + } + } + listParams := service.GetListParamsFromContext[QueryListParamParser](r.Context()) // create next cursor from lastId or afterId param if r.URL.Query().Has("lastId") { @@ -88,12 +96,20 @@ func queryV1(svc QueryService, w http.ResponseWriter, r *http.Request) error { } func queryV2(svc QueryService, w http.ResponseWriter, r *http.Request) error { - queryString := r.URL.Query().Get("q") + queryParams := r.URL.Query() + queryString := queryParams.Get("q") query, err := NewQueryFromString(queryString) if err != nil { return err } + if queryParams.Has("context") { + err = query.WithContext(queryParams.Get("context")) + if err != nil { + return service.NewInvalidParameterError("context", "invalid") + } + } + listParams := service.GetListParamsFromContext[QueryListParamParser](r.Context()) results, prevCursor, nextCursor, err := svc.Query(r.Context(), query, listParams) if err != nil { diff --git a/pkg/authz/query/parser.go b/pkg/authz/query/parser.go index b508c5c..2bcc885 100644 --- a/pkg/authz/query/parser.go +++ b/pkg/authz/query/parser.go @@ -176,6 +176,5 @@ func NewQueryFromString(queryString string) (Query, error) { } } - query.rawString = queryString return query, nil } diff --git a/pkg/authz/query/resultset.go b/pkg/authz/query/resultset.go index 36e4a05..39ba7ac 100644 --- a/pkg/authz/query/resultset.go +++ b/pkg/authz/query/resultset.go @@ -26,6 +26,7 @@ type ResultSetNode struct { ObjectId string Relation string Warrant warrant.WarrantSpec + Policy warrant.Policy IsImplicit bool next *ResultSetNode } @@ -48,7 +49,7 @@ func (rs *ResultSet) List() *ResultSetNode { return rs.head } -func (rs *ResultSet) Add(objectType string, objectId string, relation string, warrant warrant.WarrantSpec, isImplicit bool) { +func (rs *ResultSet) Add(objectType string, objectId string, relation string, warrant warrant.WarrantSpec, policy warrant.Policy, isImplicit bool) { existingRes, exists := rs.m[key(objectType, objectId, relation)] if !exists { newNode := ResultSetNode{ @@ -56,6 +57,7 @@ func (rs *ResultSet) Add(objectType string, objectId string, relation string, wa ObjectId: objectId, Relation: relation, Warrant: warrant, + Policy: policy, IsImplicit: isImplicit, next: nil, } @@ -73,9 +75,15 @@ func (rs *ResultSet) Add(objectType string, objectId string, relation string, wa // Add result node to map for O(1) lookups rs.m[key(objectType, objectId, relation)] = &newNode - } else if existingRes.IsImplicit && !isImplicit { // favor explicit results - existingRes.IsImplicit = isImplicit - existingRes.Warrant = warrant + } else { + // favor explicit results + if existingRes.IsImplicit && !isImplicit { + existingRes.IsImplicit = isImplicit + existingRes.Warrant = warrant + existingRes.Policy = policy + } + + existingRes.Policy = existingRes.Policy.Or(policy) } } @@ -95,13 +103,11 @@ func (rs *ResultSet) Has(objectType string, objectId string, relation string) bo func (rs *ResultSet) Union(other *ResultSet) *ResultSet { resultSet := NewResultSet() for iter := rs.List(); iter != nil; iter = iter.Next() { - resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Relation, iter.Warrant, iter.IsImplicit) + resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Relation, iter.Warrant, iter.Policy, iter.IsImplicit) } for iter := other.List(); iter != nil; iter = iter.Next() { - if !resultSet.Has(iter.ObjectType, iter.ObjectId, iter.Relation) || !iter.IsImplicit { - resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Relation, iter.Warrant, iter.IsImplicit) - } + resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Relation, iter.Warrant, iter.Policy, iter.IsImplicit) } return resultSet @@ -121,11 +127,14 @@ func (rs *ResultSet) Intersect(other *ResultSet) *ResultSet { for iter := a.List(); iter != nil; iter = iter.Next() { if b.Has(iter.ObjectType, iter.ObjectId, iter.Relation) { bRes := b.Get(iter.ObjectType, iter.ObjectId, iter.Relation) - if !bRes.IsImplicit { - result.Add(bRes.ObjectType, bRes.ObjectId, bRes.Relation, bRes.Warrant, bRes.IsImplicit) - } else { - result.Add(iter.ObjectType, iter.ObjectId, iter.Relation, iter.Warrant, iter.IsImplicit) - } + result.Add( + iter.ObjectType, + iter.ObjectId, + iter.Relation, + iter.Warrant, + iter.Policy.And(bRes.Policy), + bRes.IsImplicit || iter.IsImplicit, + ) } } @@ -135,11 +144,14 @@ func (rs *ResultSet) Intersect(other *ResultSet) *ResultSet { func (rs *ResultSet) String() string { var strs []string for iter := rs.List(); iter != nil; iter = iter.Next() { + resStr := fmt.Sprintf("%s => %s", key(iter.ObjectType, iter.ObjectId, iter.Relation), iter.Warrant.String()) + if iter.Policy != "" { + resStr += fmt.Sprintf("[%s]", iter.Policy) + } if iter.IsImplicit { - strs = append(strs, fmt.Sprintf("%s => %s [implicit]", key(iter.ObjectType, iter.ObjectId, iter.Relation), iter.Warrant.String())) - } else { - strs = append(strs, fmt.Sprintf("%s => %s", key(iter.ObjectType, iter.ObjectId, iter.Relation), iter.Warrant.String())) + resStr += "[implicit]" } + strs = append(strs, resStr) } return strings.Join(strs, ", ") diff --git a/pkg/authz/query/service.go b/pkg/authz/query/service.go index d3d2837..75d0c50 100644 --- a/pkg/authz/query/service.go +++ b/pkg/authz/query/service.go @@ -114,7 +114,7 @@ func (svc QueryService) Query(ctx context.Context, query Query, listParams servi } for res := queryResult.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit) } } } @@ -175,7 +175,7 @@ func (svc QueryService) Query(ctx context.Context, query Query, listParams servi } for res := queryResult.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit) } } } @@ -184,13 +184,24 @@ func (svc QueryService) Query(ctx context.Context, query Query, listParams servi } for res := resultSet.List(); res != nil; res = res.Next() { - queryResults = append(queryResults, QueryResult{ - ObjectType: res.ObjectType, - ObjectId: res.ObjectId, - Relation: res.Relation, - Warrant: res.Warrant, - IsImplicit: res.IsImplicit, - }) + var err error + addResult := true + if res.Policy != "" { + addResult, err = res.Policy.Eval(query.Context) + if err != nil { + return nil, nil, nil, err + } + } + + if addResult { + queryResults = append(queryResults, QueryResult{ + ObjectType: res.ObjectType, + ObjectId: res.ObjectId, + Relation: res.Relation, + Warrant: res.Warrant, + IsImplicit: res.IsImplicit, + }) + } } // handle sorting and pagination @@ -357,27 +368,35 @@ func (svc QueryService) query(ctx context.Context, query Query, level int) (*Res continue } + policy := matchedWarrant.Policy.And(sub.Policy) if matchedWarrant.ObjectId == warrant.Wildcard { - expandedWildcardWarrants, err := svc.listWarrants(ctx, warrant.FilterParams{ - ObjectType: matchedWarrant.ObjectType, - }) + expandedObjects, err := svc.listObjectsByType(ctx, matchedWarrant.ObjectType) if err != nil { return nil, err } - for _, w := range expandedWildcardWarrants { - if w.ObjectId != warrant.Wildcard { - resultSet.Add(w.ObjectType, w.ObjectId, relation, matchedWarrant, sub.IsImplicit || level > 0) - } + for _, obj := range expandedObjects { + resultSet.Add(obj.ObjectType, obj.ObjectId, relation, matchedWarrant, policy, sub.IsImplicit || level > 0) } } else { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, relation, matchedWarrant, sub.IsImplicit || level > 0) + resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, relation, matchedWarrant, policy, sub.IsImplicit || level > 0) } } } else if query.SelectObjects.WhereSubject == nil || (matchedWarrant.Subject.ObjectType == query.SelectObjects.WhereSubject.Type && matchedWarrant.Subject.ObjectId == query.SelectObjects.WhereSubject.Id) { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, relation, matchedWarrant, false) + if matchedWarrant.ObjectId == warrant.Wildcard { + expandedObjects, err := svc.listObjectsByType(ctx, matchedWarrant.ObjectType) + if err != nil { + return nil, err + } + + for _, obj := range expandedObjects { + resultSet.Add(obj.ObjectType, obj.ObjectId, relation, matchedWarrant, matchedWarrant.Policy, false) + } + } else { + resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, relation, matchedWarrant, matchedWarrant.Policy, false) + } } } @@ -388,7 +407,7 @@ func (svc QueryService) query(ctx context.Context, query Query, level int) (*Res } for res := implicitResultSet.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit || level > 0) } } @@ -436,10 +455,10 @@ func (svc QueryService) query(ctx context.Context, query Query, level int) (*Res } for sub := subset.List(); sub != nil; sub = sub.Next() { - resultSet.Add(sub.ObjectType, sub.ObjectId, relation, matchedWarrant, sub.IsImplicit || level > 0) + resultSet.Add(sub.ObjectType, sub.ObjectId, relation, matchedWarrant, matchedWarrant.Policy.And(sub.Policy), sub.IsImplicit || level > 0) } } else if query.SelectSubjects.SubjectTypes[0] == matchedWarrant.Subject.ObjectType { - resultSet.Add(matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.ObjectId, relation, matchedWarrant, false) + resultSet.Add(matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.ObjectId, relation, matchedWarrant, matchedWarrant.Policy, false) } } @@ -450,7 +469,7 @@ func (svc QueryService) query(ctx context.Context, query Query, level int) (*Res } for res := implicitResultSet.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit || level > 0) } } @@ -517,7 +536,7 @@ func (svc QueryService) queryRule(ctx context.Context, query Query, level int, r resultSet := NewResultSet() for res := results.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit || level > 0) } return resultSet, nil @@ -538,24 +557,55 @@ func (svc QueryService) queryRule(ctx context.Context, query Query, level int, r continue } - inheritedResults, err := svc.query(ctx, Query{ - Expand: query.Expand, - SelectObjects: &SelectObjects{ - ObjectTypes: query.SelectObjects.ObjectTypes, - WhereSubject: &Resource{ - Type: indirectWarrant.ObjectType, - Id: indirectWarrant.ObjectId, + if indirectWarrant.ObjectId == warrant.Wildcard { + expandedObjects, err := svc.listObjectsByType(ctx, indirectWarrant.ObjectType) + if err != nil { + return nil, err + } + + for _, obj := range expandedObjects { + if obj.ObjectId != indirectWarrant.Subject.ObjectId { + inheritedResults, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectObjects: &SelectObjects{ + ObjectTypes: query.SelectObjects.ObjectTypes, + WhereSubject: &Resource{ + Type: obj.ObjectType, + Id: obj.ObjectId, + }, + Relations: []string{rule.WithRelation}, + }, + Context: query.Context, + }, 0) + if err != nil { + return nil, err + } + + for res := inheritedResults.List(); res != nil; res = res.Next() { + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, indirectWarrant.Policy.Or(res.Policy), res.IsImplicit || level > 0) + } + } + } + } else { + inheritedResults, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectObjects: &SelectObjects{ + ObjectTypes: query.SelectObjects.ObjectTypes, + WhereSubject: &Resource{ + Type: indirectWarrant.ObjectType, + Id: indirectWarrant.ObjectId, + }, + Relations: []string{rule.WithRelation}, }, - Relations: []string{rule.WithRelation}, - }, - Context: query.Context, - }, 0) - if err != nil { - return nil, err - } + Context: query.Context, + }, 0) + if err != nil { + return nil, err + } - for res := inheritedResults.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + for res := inheritedResults.List(); res != nil; res = res.Next() { + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, indirectWarrant.Policy.Or(res.Policy), res.IsImplicit || level > 0) + } } } @@ -578,7 +628,7 @@ func (svc QueryService) queryRule(ctx context.Context, query Query, level int, r resultSet := NewResultSet() for res := results.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.Policy, res.IsImplicit || level > 0) } return resultSet, nil @@ -616,7 +666,7 @@ func (svc QueryService) queryRule(ctx context.Context, query Query, level int, r } for res := subset.List(); res != nil; res = res.Next() { - resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, res.IsImplicit || level > 0) + resultSet.Add(res.ObjectType, res.ObjectId, relation, res.Warrant, w.Policy.Or(res.Policy), res.IsImplicit || level > 0) } } @@ -652,6 +702,30 @@ func (svc QueryService) listWarrants(ctx context.Context, filterParams warrant.F } } +func (svc QueryService) listObjectsByType(ctx context.Context, objectType string) ([]object.ObjectSpec, error) { + var result []object.ObjectSpec + listParams := service.DefaultListParams(object.ObjectListParamParser{}) + listParams.WithLimit(MaxEdges) + for { + objectSpecs, _, nextCursor, err := svc.objectSvc.List( + ctx, + &object.FilterOptions{ObjectType: objectType}, + listParams, + ) + if err != nil { + return nil, err + } + + result = append(result, objectSpecs...) + + if nextCursor == nil { + return result, nil + } + + listParams.NextCursor = nextCursor + } +} + func objectKey(objectType string, objectId string) string { return fmt.Sprintf("%s:%s", objectType, objectId) } diff --git a/pkg/authz/query/spec.go b/pkg/authz/query/spec.go index c0ac820..145be5a 100644 --- a/pkg/authz/query/spec.go +++ b/pkg/authz/query/spec.go @@ -15,10 +15,12 @@ package authz import ( + "encoding/json" "fmt" "strings" - baseWarrant "github.com/warrant-dev/warrant/pkg/authz/warrant" + "github.com/pkg/errors" + warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" "github.com/warrant-dev/warrant/pkg/service" ) @@ -26,11 +28,21 @@ type Query struct { Expand bool SelectSubjects *SelectSubjects SelectObjects *SelectObjects - Context baseWarrant.PolicyContext - rawString string + Context warrant.PolicyContext } -func (q Query) String() string { +func (q *Query) WithContext(contextString string) error { + var context warrant.PolicyContext + err := json.Unmarshal([]byte(contextString), &context) + if err != nil { + return errors.Wrap(err, "query: error parsing query context") + } + + q.Context = context + return nil +} + +func (q *Query) String() string { var str string if q.Expand { str = "select" @@ -95,12 +107,12 @@ type QueryHaving struct { } type QueryResult struct { - ObjectType string `json:"objectType"` - ObjectId string `json:"objectId"` - Relation string `json:"relation"` - Warrant baseWarrant.WarrantSpec `json:"warrant"` - IsImplicit bool `json:"isImplicit"` - Meta map[string]interface{} `json:"meta,omitempty"` + ObjectType string `json:"objectType"` + ObjectId string `json:"objectId"` + Relation string `json:"relation"` + Warrant warrant.WarrantSpec `json:"warrant"` + IsImplicit bool `json:"isImplicit"` + Meta map[string]interface{} `json:"meta,omitempty"` } type QueryResponseV1 struct { diff --git a/tests/v2/query-policy.json b/tests/v2/query-policy.json new file mode 100644 index 0000000..6d2fdff --- /dev/null +++ b/tests/v2/query-policy.json @@ -0,0 +1,1370 @@ +{ + "ignoredFields": [ + "createdAt" + ], + "tests": [ + { + "name": "assignRoleAdminMemberOfAllRoles", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "*", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "*", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + } + } + }, + { + "name": "assignRoleAdminMemberOfPermissionManageRoles", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + } + } + }, + { + "name": "assignRoleManagerMemberOfPermissionCreateItem", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + } + }, + { + "name": "assignRoleManagerMemberOfPermissionDeleteItem", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + } + }, + { + "name": "assignRoleManagerMemberOfRoleAccountant", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "accountant", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "accountant", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + } + }, + { + "name": "assignRoleAccountantMemberOfPermissionViewBalanceSheet", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + } + } + }, + { + "name": "assignRoleAccountantMemberOfRoleReadOnly", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + } + } + }, + { + "name": "assignRoleReadOnlyMemberOfPermissionViewItems", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + } + } + }, + { + "name": "assignUserJohnMemberOfRoleAdminOnTeam1And2", + "request": { + "method": "POST", + "url": "/v2/warrants", + "headers": { + "Warrant-Token": "latest" + }, + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "john" + }, + "policy": "team in ['team-1', 'team-2']" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "john" + }, + "policy": "team in ['team-1', 'team-2']" + } + } + }, + { + "name": "assignUserJohnMemberOfRoleAdminOnTeam3", + "request": { + "method": "POST", + "url": "/v2/warrants", + "headers": { + "Warrant-Token": "latest" + }, + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "john" + }, + "policy": "team == 'team-3'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "john" + }, + "policy": "team == 'team-3'" + } + } + }, + { + "name": "assignUserJaneMemberOfRoleAccountantOnTeam1And3", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "accountant", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "jane" + }, + "policy": "team in ['team-1', 'team-3']" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "accountant", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "jane" + }, + "policy": "team in ['team-1', 'team-3']" + } + } + }, + { + "name": "assignUserJaneMemberOfRoleReadOnlyOnTeam2", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "jane" + }, + "policy": "team == 'team-2'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "jane" + }, + "policy": "team == 'team-2'" + } + } + }, + { + "name": "assignUserJamesMemberOfRoleAdminOnTeam1", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-1'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-1'" + } + } + }, + { + "name": "assignUserJamesMemberOfRoleManagerOnTeam2", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-2'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-2'" + } + } + }, + { + "name": "assignUserJamesMemberOfRoleReadOnlyOnTeam3", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-3'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "read-only", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "james" + }, + "policy": "team == 'team-3'" + } + } + }, + { + "name": "selectPermissionsWhereUserJohnIsMemberInTeam1", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:john%20is%20member&context=%7B%22team%22%3A%22team-1%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJohnIsMemberInTeam2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:john%20is%20member&context=%7B%22team%22%3A%22team-2%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJohnIsMemberInTeam3", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:john%20is%20member&context=%7B%22team%22%3A%22team-3%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJaneIsMemberInTeam1", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:jane%20is%20member&context=%7B%22team%22%3A%22team-1%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJaneIsMemberInTeam2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:jane%20is%20member&context=%7B%22team%22%3A%22team-2%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJaneIsMemberInTeam3", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:jane%20is%20member&context=%7B%22team%22%3A%22team-3%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJamesIsMemberInTeam1", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:james%20is%20member&context=%7B%22team%22%3A%22team-1%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "manage-roles", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "admin" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJamesIsMemberInTeam2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:james%20is%20member&context=%7B%22team%22%3A%22team-2%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "create-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "delete-item", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-balance-sheet", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "accountant" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserJamesIsMemberInTeam3", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:james%20is%20member&context=%7B%22team%22%3A%22team-3%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "warrant": { + "objectType": "permission", + "objectId": "view-items", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "read-only" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "deleteUserJohn", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/john" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserJane", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/jane" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserJames", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/james" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteRoleAdmin", + "request": { + "method": "DELETE", + "url": "/v2/objects/role/admin" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteRoleManager", + "request": { + "method": "DELETE", + "url": "/v2/objects/role/manager" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteRoleAccountant", + "request": { + "method": "DELETE", + "url": "/v2/objects/role/accountant" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteRoleReadOnly", + "request": { + "method": "DELETE", + "url": "/v2/objects/role/read-only" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deletePermissionManageRoles", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/manage-roles" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deletePermissionCreateItem", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/create-item" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deletePermissionDeleteItem", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/delete-item" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deletePermissionViewItems", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/view-items" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deletePermissionViewBalanceSheet", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/view-balance-sheet" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "createDocumentObjectType", + "request": { + "method": "POST", + "url": "/v2/object-types", + "body": { + "type": "document", + "relations": { + "owner": {}, + "editor": { + "inheritIf": "owner" + }, + "viewer": { + "inheritIf": "editor" + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + } + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "type": "document", + "relations": { + "owner": {}, + "editor": { + "inheritIf": "owner" + }, + "viewer": { + "inheritIf": "editor" + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + } + } + } + } + }, + { + "name": "assignUserU1OwnerOfDocumentD1InTeamT1", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + }, + "policy": "team == 'T1'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + }, + "policy": "team == 'T1'" + } + } + }, + { + "name": "assignUserU2ViewerOfDocumentD1InTeamT2", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U2" + }, + "policy": "team == 'T2'" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U2" + }, + "policy": "team == 'T2'" + } + } + }, + { + "name": "selectDocumentsWhereUserU1IsEditorViewerInTeamT1", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20document%20where%20user:U1%20is%20editor-viewer&context=%7B%22team%22%3A%22T1%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D1", + "relation": "editor-viewer", + "warrant": { + "objectType": "document", + "objectId": "D1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + }, + "policy": "team == 'T1'" + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU1IsEditorViewerInTeamT2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20document%20where%20user:U1%20is%20editor-viewer&context=%7B%22team%22%3A%22T2%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [] + } + } + }, + { + "name": "selectDocumentsWhereUserU2IsEditorViewerInTeamT1", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20document%20where%20user:U2%20is%20editor-viewer&context=%7B%22team%22%3A%22T1%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [] + } + } + }, + { + "name": "selectDocumentsWhereUserU2IsViewerInTeamT2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20document%20where%20user:U2%20is%20viewer&context=%7B%22team%22%3A%22T2%22%7D" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D1", + "relation": "viewer", + "warrant": { + "objectType": "document", + "objectId": "D1", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U2" + }, + "policy": "team == 'T2'" + }, + "isImplicit": false + } + ] + } + } + }, + { + "name": "deleteDocumentD1", + "request": { + "method": "DELETE", + "url": "/v2/objects/document/D1" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUser1", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/U1" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUser2", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/U2" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteObjectTypeDocument", + "request": { + "method": "DELETE", + "url": "/v2/object-types/document" + }, + "expectedResponse": { + "statusCode": 200 + } + } + ] +}