From 60307945f14c53ec92991d61b2ad3454f7ed9e63 Mon Sep 17 00:00:00 2001 From: Karan Kajla Date: Tue, 5 Mar 2024 14:35:00 -0800 Subject: [PATCH] Rewrite + Improve Query Execution (#302) --- pkg/authz/check/service.go | 66 ++-- pkg/authz/query/handlers.go | 4 +- pkg/authz/query/parser.go | 79 ++-- pkg/authz/query/resultset.go | 22 +- pkg/authz/query/service.go | 599 ++++++++++++++++++------------- pkg/authz/query/spec.go | 42 ++- pkg/authz/warrant/handlers.go | 13 +- pkg/authz/warrant/list.go | 26 +- pkg/authz/warrant/mysql.go | 56 +-- pkg/authz/warrant/postgres.go | 56 +-- pkg/authz/warrant/sqlite.go | 46 +-- pkg/service/list.go | 52 ++- tests/v1/query.json | 347 ++++++++++++++---- tests/v1/warrants-list.json | 39 -- tests/v2/query.json | 658 ++++++++++++++++++++++++++-------- tests/v2/warrants-list.json | 41 --- 16 files changed, 1378 insertions(+), 768 deletions(-) diff --git a/pkg/authz/check/service.go b/pkg/authz/check/service.go index c37b8b4c..2c3c0cdc 100644 --- a/pkg/authz/check/service.go +++ b/pkg/authz/check/service.go @@ -73,16 +73,16 @@ func (svc CheckService) getWithPolicyMatch(ctx context.Context, checkPipeline *p defer checkPipeline.ReleaseServiceLock() listParams := service.DefaultListParams(warrant.WarrantListParamParser{}) - listParams.Limit = MaxWarrants + listParams.WithLimit(MaxWarrants) warrantSpecs, _, _, err := svc.warrantSvc.List( ctx, warrant.FilterParams{ - ObjectType: []string{spec.ObjectType}, - ObjectId: []string{spec.ObjectId}, - Relation: []string{spec.Relation}, - SubjectType: []string{spec.Subject.ObjectType}, - SubjectId: []string{spec.Subject.ObjectId}, - SubjectRelation: []string{spec.Subject.Relation}, + ObjectType: spec.ObjectType, + ObjectId: spec.ObjectId, + Relation: spec.Relation, + SubjectType: spec.Subject.ObjectType, + SubjectId: spec.Subject.ObjectId, + SubjectRelation: spec.Subject.Relation, }, listParams, ) @@ -91,16 +91,16 @@ func (svc CheckService) getWithPolicyMatch(ctx context.Context, checkPipeline *p } // if a warrant without a policy is found, match it - for _, warrant := range warrantSpecs { - if warrant.Policy == "" { - return &warrant, nil + for _, w := range warrantSpecs { + if w.Policy == "" { + return &w, nil } } - for _, warrant := range warrantSpecs { - if warrant.Policy != "" { - if policyMatched := evalWarrantPolicy(warrant, spec.Context); policyMatched { - return &warrant, nil + for _, w := range warrantSpecs { + if w.Policy != "" { + if policyMatched := evalWarrantPolicy(w, spec.Context); policyMatched { + return &w, nil } } } @@ -123,13 +123,13 @@ func (svc CheckService) getMatchingSubjects(ctx context.Context, checkPipeline * } listParams := service.DefaultListParams(warrant.WarrantListParamParser{}) - listParams.Limit = MaxWarrants + listParams.WithLimit(MaxWarrants) warrantSpecs, _, _, err = svc.warrantSvc.List( ctx, warrant.FilterParams{ - ObjectType: []string{objectType}, - ObjectId: []string{objectId}, - Relation: []string{relation}, + ObjectType: objectType, + ObjectId: objectId, + Relation: relation, }, listParams, ) @@ -138,12 +138,12 @@ func (svc CheckService) getMatchingSubjects(ctx context.Context, checkPipeline * } matchingSpecs := make([]warrant.WarrantSpec, 0) - for _, warrant := range warrantSpecs { - if warrant.Policy == "" { - matchingSpecs = append(matchingSpecs, warrant) + for _, w := range warrantSpecs { + if w.Policy == "" { + matchingSpecs = append(matchingSpecs, w) } else { - if policyMatched := evalWarrantPolicy(warrant, checkCtx); policyMatched { - matchingSpecs = append(matchingSpecs, warrant) + if policyMatched := evalWarrantPolicy(w, checkCtx); policyMatched { + matchingSpecs = append(matchingSpecs, w) } } } @@ -167,14 +167,14 @@ func (svc CheckService) getMatchingSubjectsBySubjectType(ctx context.Context, ch } listParams := service.DefaultListParams(warrant.WarrantListParamParser{}) - listParams.Limit = MaxWarrants + listParams.WithLimit(MaxWarrants) warrantSpecs, _, _, err = svc.warrantSvc.List( ctx, warrant.FilterParams{ - ObjectType: []string{objectType}, - ObjectId: []string{objectId}, - Relation: []string{relation}, - SubjectType: []string{subjectType}, + ObjectType: objectType, + ObjectId: objectId, + Relation: relation, + SubjectType: subjectType, }, listParams, ) @@ -183,12 +183,12 @@ func (svc CheckService) getMatchingSubjectsBySubjectType(ctx context.Context, ch } matchingSpecs := make([]warrant.WarrantSpec, 0) - for _, warrant := range warrantSpecs { - if warrant.Policy == "" { - matchingSpecs = append(matchingSpecs, warrant) + for _, w := range warrantSpecs { + if w.Policy == "" { + matchingSpecs = append(matchingSpecs, w) } else { - if policyMatched := evalWarrantPolicy(warrant, checkCtx); policyMatched { - matchingSpecs = append(matchingSpecs, warrant) + if policyMatched := evalWarrantPolicy(w, checkCtx); policyMatched { + matchingSpecs = append(matchingSpecs, w) } } } diff --git a/pkg/authz/query/handlers.go b/pkg/authz/query/handlers.go index 6fa3249d..54a6ef18 100644 --- a/pkg/authz/query/handlers.go +++ b/pkg/authz/query/handlers.go @@ -56,14 +56,14 @@ func queryV1(svc QueryService, w http.ResponseWriter, r *http.Request) error { return service.NewInvalidParameterError("lastId", "invalid lastId") } - listParams.NextCursor = lastIdCursor + listParams.WithNextCursor(lastIdCursor) } else if r.URL.Query().Has("afterId") { afterIdCursor, err := service.NewCursorFromBase64String(r.URL.Query().Get("afterId"), QueryListParamParser{}, listParams.SortBy) if err != nil { return service.NewInvalidParameterError("afterId", "invalid afterId") } - listParams.NextCursor = afterIdCursor + listParams.WithNextCursor(afterIdCursor) } results, _, nextCursor, err := svc.Query(r.Context(), query, listParams) diff --git a/pkg/authz/query/parser.go b/pkg/authz/query/parser.go index 0cd6bce3..b508c5ce 100644 --- a/pkg/authz/query/parser.go +++ b/pkg/authz/query/parser.go @@ -84,44 +84,44 @@ func (parser parser) Parse(query string) (*ast, error) { return ast, nil } -func NewQueryFromString(queryString string) (*Query, error) { +func NewQueryFromString(queryString string) (Query, error) { var query Query queryParser, err := newParser() if err != nil { - return nil, errors.Wrap(err, "error creating query from string") + return Query{}, errors.Wrap(err, "error creating query from string") } ast, err := queryParser.Parse(queryString) if err != nil { - return nil, service.NewInvalidParameterError("q", err.Error()) + return Query{}, service.NewInvalidParameterError("q", err.Error()) } if ast.SelectClause == nil { - return nil, service.NewInvalidParameterError("q", "must contain a 'select' clause") + return Query{}, service.NewInvalidParameterError("q", "must contain a 'select' clause") } if ast.SelectClause.ObjectTypesOrRelations == nil && ast.SelectClause.SubjectTypes == nil { - return nil, service.NewInvalidParameterError("q", "incomplete 'select' clause") + return Query{}, service.NewInvalidParameterError("q", "incomplete 'select' clause") } if ast.ForClause != nil && ast.WhereClause != nil { - return nil, service.NewInvalidParameterError("q", "cannot contain both a 'for' clause and a 'where' clause") + return Query{}, service.NewInvalidParameterError("q", "cannot contain both a 'for' clause and a 'where' clause") } query.Expand = !ast.SelectClause.Explicit if ast.SelectClause.SubjectTypes != nil { // Querying for subjects if len(ast.SelectClause.SubjectTypes) == 0 { - return nil, service.NewInvalidParameterError("q", "must contain one or more types of subjects to select") + return Query{}, service.NewInvalidParameterError("q", "must contain one or more types of subjects to select") } if ast.SelectClause.ObjectTypesOrRelations == nil || len(ast.SelectClause.ObjectTypesOrRelations) == 0 { - return nil, service.NewInvalidParameterError("q", "must select one or more relations for subjects to match on the object") + return Query{}, service.NewInvalidParameterError("q", "must select one or more relations for subjects to match on the object") } if ast.WhereClause != nil { - return nil, service.NewInvalidParameterError("q", "cannot contain a 'where' clause when selecting subjects") + return Query{}, service.NewInvalidParameterError("q", "cannot contain a 'where' clause when selecting subjects") } query.SelectSubjects = &SelectSubjects{ @@ -129,24 +129,26 @@ func NewQueryFromString(queryString string) (*Query, error) { SubjectTypes: ast.SelectClause.SubjectTypes, } - if ast.ForClause != nil { - objectType, objectId, colonFound := strings.Cut(ast.ForClause.Object, ":") - if !colonFound { - return nil, service.NewInvalidParameterError("q", "'for' clause contains invalid object") - } - - query.SelectSubjects.ForObject = &Resource{ - Type: objectType, - Id: objectId, - } + if ast.ForClause == nil { + return Query{}, service.NewInvalidParameterError("q", "must contain a 'for' clause") + } + + objectType, objectId, colonFound := strings.Cut(ast.ForClause.Object, ":") + if !colonFound { + return Query{}, service.NewInvalidParameterError("q", "'for' clause contains invalid object") + } + + query.SelectSubjects.ForObject = &Resource{ + Type: objectType, + Id: objectId, } } else { // Querying for objects if ast.SelectClause.ObjectTypesOrRelations == nil || len(ast.SelectClause.ObjectTypesOrRelations) == 0 { - return nil, service.NewInvalidParameterError("q", "must contain one or more types of objects to select") + return Query{}, service.NewInvalidParameterError("q", "must contain one or more types of objects to select") } if ast.ForClause != nil { - return nil, service.NewInvalidParameterError("q", "cannot contain a 'for' clause when selecting objects") + return Query{}, service.NewInvalidParameterError("q", "cannot contain a 'for' clause when selecting objects") } query.SelectObjects = &SelectObjects{ @@ -154,25 +156,26 @@ func NewQueryFromString(queryString string) (*Query, error) { Relations: []string{warrant.Wildcard}, } - if ast.WhereClause != nil { - if ast.WhereClause.Relations == nil || len(ast.WhereClause.Relations) == 0 { - return nil, service.NewInvalidParameterError("q", "must contain one or more relations the subject must have on matching objects") - } - - subjectType, subjectId, colonFound := strings.Cut(ast.WhereClause.Subject, ":") - if !colonFound { - return nil, service.NewInvalidParameterError("q", "'where' clause contains invalid subject") - } - - query.SelectObjects.Relations = ast.WhereClause.Relations - query.SelectObjects.WhereSubject = &Resource{ - Type: subjectType, - Id: subjectId, - } + if ast.WhereClause == nil { + return Query{}, service.NewInvalidParameterError("q", "must contain a 'where' clause") + } + + if ast.WhereClause.Relations == nil || len(ast.WhereClause.Relations) == 0 { + return Query{}, service.NewInvalidParameterError("q", "must contain one or more relations the subject must have on matching objects") + } + + subjectType, subjectId, colonFound := strings.Cut(ast.WhereClause.Subject, ":") + if !colonFound { + return Query{}, service.NewInvalidParameterError("q", "'where' clause contains invalid subject") + } + + query.SelectObjects.Relations = ast.WhereClause.Relations + query.SelectObjects.WhereSubject = &Resource{ + Type: subjectType, + Id: subjectId, } } query.rawString = queryString - - return &query, nil + return query, nil } diff --git a/pkg/authz/query/resultset.go b/pkg/authz/query/resultset.go index 11435f04..36b86eb7 100644 --- a/pkg/authz/query/resultset.go +++ b/pkg/authz/query/resultset.go @@ -40,20 +40,24 @@ type ResultSet struct { } func (rs *ResultSet) List() *ResultSetNode { + if rs == nil { + return nil + } + return rs.head } func (rs *ResultSet) Add(objectType string, objectId string, warrant warrant.WarrantSpec, isImplicit bool) { - if _, exists := rs.m[key(objectType, objectId)]; !exists { - // Add warrant to list - newNode := &ResultSetNode{ - ObjectType: objectType, - ObjectId: objectId, - Warrant: warrant, - IsImplicit: isImplicit, - next: nil, - } + newNode := &ResultSetNode{ + ObjectType: objectType, + ObjectId: objectId, + Warrant: warrant, + IsImplicit: isImplicit, + next: nil, + } + if existingRes, exists := rs.m[key(objectType, objectId)]; !exists || (existingRes.IsImplicit && !isImplicit) { + // Add warrant to list if rs.head == nil { rs.head = newNode } diff --git a/pkg/authz/query/service.go b/pkg/authz/query/service.go index dda783d7..7448f289 100644 --- a/pkg/authz/query/service.go +++ b/pkg/authz/query/service.go @@ -16,11 +16,11 @@ package authz import ( "context" - "errors" "fmt" "sort" "strings" + "github.com/pkg/errors" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" "github.com/warrant-dev/warrant/pkg/object" @@ -50,7 +50,7 @@ func NewService(env service.Env, objectTypeSvc objecttype.Service, warrantSvc wa } } -func (svc QueryService) Query(ctx context.Context, query *Query, listParams service.ListParams) ([]QueryResult, *service.Cursor, *service.Cursor, error) { +func (svc QueryService) Query(ctx context.Context, query Query, listParams service.ListParams) ([]QueryResult, *service.Cursor, *service.Cursor, error) { queryResults := make([]QueryResult, 0) resultMap := make(map[string]int) objects := make(map[string][]string) @@ -60,9 +60,123 @@ func (svc QueryService) Query(ctx context.Context, query *Query, listParams serv return nil, nil, nil, ErrInvalidQuery } - resultSet, err := svc.query(ctx, query) - if err != nil { - return nil, nil, nil, err + resultSet := NewResultSet() + switch { + case query.SelectObjects != nil: + var ( + objectTypes []objecttype.ObjectTypeSpec + err error + ) + if query.SelectObjects.ObjectTypes[0] == warrant.Wildcard { + objectTypesListParams := service.DefaultListParams(objecttype.ObjectTypeListParamParser{}) + objectTypesListParams.WithLimit(MaxObjectTypes) + objectTypes, _, _, err = svc.objectTypeSvc.List(ctx, objectTypesListParams) + if err != nil { + return nil, nil, nil, err + } + } else { + for _, typeId := range query.SelectObjects.ObjectTypes { + objectType, err := svc.objectTypeSvc.GetByTypeId(ctx, typeId) + if err != nil { + return nil, nil, nil, err + } + + objectTypes = append(objectTypes, *objectType) + } + } + + for _, objectType := range objectTypes { + var relations []string + if query.SelectObjects.Relations[0] == warrant.Wildcard { + for relation := range objectType.Relations { + relations = append(relations, relation) + } + } else { + for _, relation := range query.SelectObjects.Relations { + if _, ok := objectType.Relations[relation]; ok { + relations = append(relations, relation) + } + } + } + + for _, relation := range relations { + res, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectObjects: &SelectObjects{ + ObjectTypes: []string{objectType.Type}, + Relations: []string{relation}, + WhereSubject: query.SelectObjects.WhereSubject, + }, + Context: query.Context, + }, 0) + if err != nil { + return nil, nil, nil, err + } + + resultSet = resultSet.Union(res) + } + } + case query.SelectSubjects != nil: + var ( + subjectTypes []objecttype.ObjectTypeSpec + err error + ) + if query.SelectSubjects.SubjectTypes[0] == warrant.Wildcard { + objectTypesListParams := service.DefaultListParams(objecttype.ObjectTypeListParamParser{}) + objectTypesListParams.WithLimit(MaxObjectTypes) + subjectTypes, _, _, err = svc.objectTypeSvc.List(ctx, objectTypesListParams) + if err != nil { + return nil, nil, nil, err + } + } else { + for _, typeId := range query.SelectSubjects.SubjectTypes { + subjectType, err := svc.objectTypeSvc.GetByTypeId(ctx, typeId) + if err != nil { + return nil, nil, nil, err + } + + subjectTypes = append(subjectTypes, *subjectType) + } + } + + objectType, err := svc.objectTypeSvc.GetByTypeId(ctx, query.SelectSubjects.ForObject.Type) + if err != nil { + return nil, nil, nil, err + } + + var relations []string + if query.SelectSubjects.Relations[0] == warrant.Wildcard { + for relation := range objectType.Relations { + relations = append(relations, relation) + } + } else { + for _, relation := range query.SelectSubjects.Relations { + if _, ok := objectType.Relations[relation]; ok { + relations = append(relations, relation) + } + } + } + + for _, subjectType := range subjectTypes { + for _, relation := range relations { + res, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectSubjects: &SelectSubjects{ + SubjectTypes: []string{subjectType.Type}, + Relations: []string{relation}, + ForObject: query.SelectSubjects.ForObject, + }, + Context: query.Context, + }, 0) + if err != nil { + return nil, nil, nil, err + } + + resultSet = resultSet.Union(res) + } + } + default: + return nil, nil, nil, ErrInvalidQuery } for res := resultSet.List(); res != nil; res = res.Next() { @@ -186,207 +300,105 @@ func (svc QueryService) Query(ctx context.Context, query *Query, listParams serv return paginatedQueryResults, prevCursor, nextCursor, nil } -func (svc QueryService) query(ctx context.Context, query *Query) (*ResultSet, error) { - var objectTypes []string - var selectSubjects bool - objectTypesListParams := service.DefaultListParams(objecttype.ObjectTypeListParamParser{}) - objectTypesListParams.Limit = MaxObjectTypes - typesList, _, _, err := svc.objectTypeSvc.List(ctx, objectTypesListParams) - if err != nil { - return nil, err - } - //nolint:gocritic - if query.SelectObjects != nil { - selectSubjects = false - if query.SelectObjects.ObjectTypes[0] == warrant.Wildcard { - for _, objectType := range typesList { - objectTypes = append(objectTypes, objectType.Type) - } - } else { - objectTypes = append(objectTypes, query.SelectObjects.ObjectTypes...) - } - } else if query.SelectSubjects != nil { - selectSubjects = true - if query.SelectSubjects.ForObject != nil { - objectTypes = append(objectTypes, query.SelectSubjects.ForObject.Type) - } else { - for _, objectType := range typesList { - objectTypes = append(objectTypes, objectType.Type) - } - } - } else { - return nil, ErrInvalidQuery - } - - resultSet := NewResultSet() - for _, objectType := range objectTypes { - var relations []string +func (svc QueryService) query(ctx context.Context, query Query, level int) (*ResultSet, error) { + switch { + case query.SelectObjects != nil: + objectType := query.SelectObjects.ObjectTypes[0] + relation := query.SelectObjects.Relations[0] objectTypeDef, err := svc.objectTypeSvc.GetByTypeId(ctx, objectType) if err != nil { return nil, err } - if query.SelectObjects != nil { - if query.SelectObjects.Relations[0] == warrant.Wildcard { - for relation := range objectTypeDef.Relations { - relations = append(relations, relation) - } - } else { - relations = append(relations, query.SelectObjects.Relations...) - } - } else { - if query.SelectSubjects.Relations[0] == warrant.Wildcard { - for relation := range objectTypeDef.Relations { - relations = append(relations, relation) - } - } else { - relations = append(relations, query.SelectSubjects.Relations...) - } + if _, found := objectTypeDef.Relations[relation]; !found { + return nil, errors.New(fmt.Sprintf("query: relation %s does not exist on object type %s", relation, objectType)) } - for _, relation := range relations { - var matchFilters warrant.FilterParams - if query.SelectObjects != nil && query.SelectObjects.WhereSubject != nil { - matchFilters.SubjectType = []string{query.SelectObjects.WhereSubject.Type} - - if query.SelectObjects.WhereSubject.Id != warrant.Wildcard { - matchFilters.SubjectId = []string{query.SelectObjects.WhereSubject.Id} - } - } else if query.SelectSubjects != nil && query.SelectSubjects.ForObject != nil { - matchFilters.ObjectType = []string{query.SelectSubjects.ForObject.Type} - - if query.SelectSubjects.ForObject.Id != warrant.Wildcard { - matchFilters.ObjectId = []string{query.SelectSubjects.ForObject.Id} - } - - if query.SelectSubjects.SubjectTypes[0] != warrant.Wildcard { - matchFilters.SubjectType = query.SelectSubjects.SubjectTypes - } - } - - res, err := svc.matchRelation(ctx, selectSubjects, objectType, relation, matchFilters, query.Expand, 0) - if err != nil { - return nil, err - } - - resultSet = resultSet.Union(res) + // base case: explicit query + matchedWarrants, err := svc.listWarrants(ctx, warrant.FilterParams{ + ObjectType: query.SelectObjects.ObjectTypes[0], + Relation: query.SelectObjects.Relations[0], + }) + if err != nil { + return nil, err } - } - return resultSet, nil -} - -func (svc QueryService) matchRelation(ctx context.Context, selectSubjects bool, objectType string, relation string, matchFilters warrant.FilterParams, expand bool, level int) (*ResultSet, error) { - objectTypeDef, err := svc.objectTypeSvc.GetByTypeId(ctx, objectType) - if err != nil { - return nil, err - } - - resultSet := NewResultSet() - if _, exists := objectTypeDef.Relations[relation]; !exists { - return resultSet, nil - } - - // match any warrants at this level - matchedWarrants, _, _, err := svc.matchWarrants(ctx, warrant.FilterParams{ - ObjectType: []string{objectType}, - ObjectId: matchFilters.ObjectId, - Relation: []string{relation}, - }) - if err != nil { - return nil, err - } - - for _, matchedWarrant := range matchedWarrants { - // match any encountered group warrants - //nolint:gocritic - if matchedWarrant.Subject.Relation != "" { - // only expand group warrants if requested - if !expand { - continue - } + resultSet := NewResultSet() + for _, matchedWarrant := range matchedWarrants { + if matchedWarrant.Subject.Relation != "" { + // handle group warrants + userset, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectSubjects: &SelectSubjects{ + Relations: []string{matchedWarrant.Subject.Relation}, + SubjectTypes: []string{query.SelectObjects.WhereSubject.Type}, + ForObject: &Resource{ + Type: matchedWarrant.Subject.ObjectType, + Id: matchedWarrant.Subject.ObjectId, + }, + }, + Context: query.Context, + }, 0) + if err != nil { + return nil, err + } - res, err := svc.matchRelation(ctx, selectSubjects, matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.Relation, warrant.FilterParams{ - ObjectId: []string{matchedWarrant.Subject.ObjectId}, - SubjectType: matchFilters.SubjectType, - SubjectId: matchFilters.SubjectId, - }, expand, level+1) - if err != nil { - return nil, err - } + for res := userset.List(); res != nil; res = res.Next() { + if res.ObjectType != query.SelectObjects.WhereSubject.Type || res.ObjectId != query.SelectObjects.WhereSubject.Id { + continue + } - if selectSubjects { - resultSet = resultSet.Union(res) - } else if res.Len() > 0 { - //nolint:gocritic - if matchedWarrant.ObjectId != warrant.Wildcard { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant, level > 0) - } else if len(matchFilters.ObjectId) > 0 { - resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant, level > 0) - } else { - //nolint:gocritic - if matchedWarrant.ObjectId != warrant.Wildcard { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant, level > 0) - } else if len(matchFilters.ObjectId) > 0 { - resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant, level > 0) - } else { - wcWarrantMatches, _, _, err := svc.matchWarrants(ctx, warrant.FilterParams{ - ObjectType: []string{matchedWarrant.ObjectType}, + if matchedWarrant.ObjectId == warrant.Wildcard { + expandedWildcardWarrants, err := svc.listWarrants(ctx, warrant.FilterParams{ + ObjectType: matchedWarrant.ObjectType, }) if err != nil { return nil, err } - for _, wcWarrantMatch := range wcWarrantMatches { - if wcWarrantMatch.ObjectId != warrant.Wildcard { - resultSet.Add(wcWarrantMatch.ObjectType, wcWarrantMatch.ObjectId, matchedWarrant, level > 0) + for _, w := range expandedWildcardWarrants { + if w.ObjectId != warrant.Wildcard { + resultSet.Add(w.ObjectType, w.ObjectId, matchedWarrant, level > 0) } } + } else { + resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant, 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, matchedWarrant, level > 0) } - } else if selectSubjects && matches(matchFilters.SubjectType, matchedWarrant.Subject.ObjectType) { - resultSet.Add(matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.ObjectId, matchedWarrant, level > 0) - } else if matches(matchFilters.SubjectType, matchedWarrant.Subject.ObjectType) && matches(matchFilters.SubjectId, matchedWarrant.Subject.ObjectId) { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant, level > 0) } - } - // explore following levels if requested - if expand { - rule := objectTypeDef.Relations[relation] - res, err := svc.matchRule(ctx, selectSubjects, objectType, relation, &rule, matchFilters, expand, level+1) + if query.Expand { + implicitResultSet, err := svc.queryRule(ctx, query, level, objectTypeDef.Relations[relation]) + if err != nil { + return nil, err + } + + resultSet = resultSet.Union(implicitResultSet) + } + + return resultSet, nil + case query.SelectSubjects != nil: + objectType := query.SelectSubjects.ForObject.Type + relation := query.SelectSubjects.Relations[0] + objectTypeDef, err := svc.objectTypeSvc.GetByTypeId(ctx, objectType) if err != nil { return nil, err } - resultSet = resultSet.Union(res) - } - - return resultSet, nil -} -func (svc QueryService) matchRule(ctx context.Context, selectSubjects bool, objectType string, relation string, rule *objecttype.RelationRule, matchFilters warrant.FilterParams, expand bool, level int) (*ResultSet, error) { - switch rule.InheritIf { - case "": - // Do nothing, explicit matches already explored in matchRelation - return NewResultSet(), nil - case objecttype.InheritIfAllOf, objecttype.InheritIfAnyOf, objecttype.InheritIfNoneOf: - return svc.matchSetRule(ctx, selectSubjects, objectType, relation, rule.InheritIf, rule.Rules, matchFilters, expand, level) - default: - // inherit relation if subject has: - // (1) InheritIf on this object - if rule.OfType == "" && rule.WithRelation == "" { - return svc.matchRelation(ctx, selectSubjects, objectType, rule.InheritIf, matchFilters, expand, level+1) + if _, found := objectTypeDef.Relations[relation]; !found { + return nil, errors.New(fmt.Sprintf("query: relation %s does not exist on object type %s", relation, objectType)) } - // inherit relation if subject has: - // (1) InheritIf on object (2) of type OfType - // (3) with relation WithRelation on this object - matchedWarrants, _, _, err := svc.matchWarrants(ctx, warrant.FilterParams{ - ObjectType: []string{objectType}, - Relation: []string{rule.WithRelation}, - ObjectId: matchFilters.ObjectId, - SubjectType: []string{rule.OfType}, + // base case: explicit query + matchedWarrants, err := svc.listWarrants(ctx, warrant.FilterParams{ + ObjectType: query.SelectSubjects.ForObject.Type, + ObjectId: query.SelectSubjects.ForObject.Id, + Relation: query.SelectSubjects.Relations[0], }) if err != nil { return nil, err @@ -394,70 +406,59 @@ func (svc QueryService) matchRule(ctx context.Context, selectSubjects bool, obje resultSet := NewResultSet() for _, matchedWarrant := range matchedWarrants { - res, err := svc.matchRelation(ctx, selectSubjects, rule.OfType, rule.InheritIf, warrant.FilterParams{ - ObjectType: matchFilters.ObjectType, - ObjectId: []string{matchedWarrant.Subject.ObjectId}, - SubjectType: matchFilters.SubjectType, - SubjectId: matchFilters.SubjectId, - }, expand, level+1) + if matchedWarrant.Subject.Relation != "" { + // handle group warrants + userset, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectSubjects: &SelectSubjects{ + Relations: []string{matchedWarrant.Subject.Relation}, + SubjectTypes: query.SelectSubjects.SubjectTypes, + ForObject: &Resource{ + Type: matchedWarrant.Subject.ObjectType, + Id: matchedWarrant.Subject.ObjectId, + }, + }, + Context: query.Context, + }, 0) + if err != nil { + return nil, err + } + + for res := userset.List(); res != nil; res = res.Next() { + resultSet.Add(res.ObjectType, res.ObjectId, matchedWarrant, level > 0) + } + } else if query.SelectSubjects.SubjectTypes[0] == matchedWarrant.Subject.ObjectType { + resultSet.Add(matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.ObjectId, matchedWarrant, level > 0) + } + } + + if query.Expand { + implicitResultSet, err := svc.queryRule(ctx, query, level, objectTypeDef.Relations[relation]) if err != nil { return nil, err } - if selectSubjects { - resultSet = resultSet.Union(res) - } else if res.Len() > 0 { - //nolint:gocritic - if matchedWarrant.ObjectId != warrant.Wildcard { - resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant, level > 0) - } else if len(matchFilters.ObjectId) > 0 { - resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant, level > 0) - } else { - wcWarrantMatches, _, _, err := svc.matchWarrants(ctx, warrant.FilterParams{ - ObjectType: []string{matchedWarrant.ObjectType}, - }) - if err != nil { - return nil, err - } - - for _, wcWarrantMatch := range wcWarrantMatches { - if wcWarrantMatch.ObjectId != warrant.Wildcard { - resultSet.Add(wcWarrantMatch.ObjectType, wcWarrantMatch.ObjectId, matchedWarrant, level > 0) - } - } - } - } + return resultSet.Union(implicitResultSet), nil } return resultSet, nil + default: + return nil, ErrInvalidQuery } } -func (svc QueryService) matchSetRule( - ctx context.Context, - selectSubjects bool, - objectType string, - relation string, - setRuleType string, - rules []objecttype.RelationRule, - matchFilters warrant.FilterParams, - expand bool, - level int, -) (*ResultSet, error) { - switch setRuleType { +func (svc QueryService) queryRule(ctx context.Context, query Query, level int, rule objecttype.RelationRule) (*ResultSet, error) { + switch rule.InheritIf { + case "": + return NewResultSet(), nil case objecttype.InheritIfAllOf: var resultSet *ResultSet - for i := range rules { - res, err := svc.matchRule(ctx, selectSubjects, objectType, relation, &rules[i], matchFilters, expand, level) + for _, r := range rule.Rules { + res, err := svc.queryRule(ctx, query, level, r) if err != nil { return nil, err } - // short-circuit if no matches found for a rule - if res.Len() == 0 { - return NewResultSet(), nil - } - if resultSet == nil { resultSet = res } else { @@ -467,41 +468,151 @@ func (svc QueryService) matchSetRule( return resultSet, nil case objecttype.InheritIfAnyOf: - resultSet := NewResultSet() - for i := range rules { - res, err := svc.matchRule(ctx, selectSubjects, objectType, relation, &rules[i], matchFilters, expand, level) + var resultSet *ResultSet + for _, r := range rule.Rules { + res, err := svc.queryRule(ctx, query, level, r) if err != nil { return nil, err } - resultSet = resultSet.Union(res) + + if resultSet == nil { + resultSet = res + } else { + resultSet = resultSet.Union(res) + } } return resultSet, nil case objecttype.InheritIfNoneOf: return nil, service.NewInvalidRequestError("cannot query authorization models with object types that use the 'noneOf' operator.") default: - return nil, ErrInvalidQuery + switch { + case query.SelectObjects != nil: + if rule.OfType == "" && rule.WithRelation == "" { + return svc.query(ctx, Query{ + Expand: true, + SelectObjects: &SelectObjects{ + ObjectTypes: query.SelectObjects.ObjectTypes, + WhereSubject: query.SelectObjects.WhereSubject, + Relations: []string{rule.InheritIf}, + }, + Context: query.Context, + }, level+1) + } else { + indirectWarrants, err := svc.listWarrants(ctx, warrant.FilterParams{ + ObjectType: rule.OfType, + Relation: rule.InheritIf, + SubjectType: query.SelectObjects.WhereSubject.Type, + SubjectId: query.SelectObjects.WhereSubject.Id, + }) + if err != nil { + return nil, err + } + + resultSet := NewResultSet() + for _, indirectWarrant := range indirectWarrants { + if indirectWarrant.Subject.Relation != "" { + continue + } + + 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}, + }, + Context: query.Context, + }, level+1) + if err != nil { + return nil, err + } + + resultSet = resultSet.Union(inheritedResults) + } + + return resultSet, nil + } + case query.SelectSubjects != nil: + if rule.OfType == "" && rule.WithRelation == "" { + return svc.query(ctx, Query{ + Expand: true, + SelectSubjects: &SelectSubjects{ + SubjectTypes: query.SelectSubjects.SubjectTypes, + Relations: []string{rule.InheritIf}, + ForObject: query.SelectSubjects.ForObject, + }, + Context: query.Context, + }, level+1) + } else { + userset, err := svc.listWarrants(ctx, warrant.FilterParams{ + ObjectType: query.SelectSubjects.ForObject.Type, + ObjectId: query.SelectSubjects.ForObject.Id, + Relation: rule.WithRelation, + SubjectType: rule.OfType, + }) + if err != nil { + return nil, err + } + + resultSet := NewResultSet() + for _, w := range userset { + if w.Subject.Relation != "" { + continue + } + + subset, err := svc.query(ctx, Query{ + Expand: query.Expand, + SelectSubjects: &SelectSubjects{ + SubjectTypes: query.SelectSubjects.SubjectTypes, + Relations: []string{rule.InheritIf}, + ForObject: &Resource{ + Type: w.Subject.ObjectType, + Id: w.Subject.ObjectId, + }, + }, + Context: query.Context, + }, level+1) + if err != nil { + return nil, err + } + + resultSet = resultSet.Union(subset) + } + + return resultSet, nil + } + default: + return nil, ErrInvalidQuery + } } } -func (svc QueryService) matchWarrants(ctx context.Context, matchFilters warrant.FilterParams) ([]warrant.WarrantSpec, *service.Cursor, *service.Cursor, error) { - warrantListParams := service.DefaultListParams(warrant.WarrantListParamParser{}) - warrantListParams.Limit = MaxEdges - return svc.warrantSvc.List(ctx, matchFilters, warrantListParams) -} +func (svc QueryService) listWarrants(ctx context.Context, filterParams warrant.FilterParams) ([]warrant.WarrantSpec, error) { + var result []warrant.WarrantSpec + listParams := service.DefaultListParams(warrant.WarrantListParamParser{}) + listParams.WithLimit(MaxEdges) + for { + warrantSpecs, _, nextCursor, err := svc.warrantSvc.List( + ctx, + filterParams, + listParams, + ) + if err != nil { + return nil, err + } -func matches(set []string, target string) bool { - if len(set) == 0 || target == warrant.Wildcard { - return true - } + result = append(result, warrantSpecs...) - for _, val := range set { - if val == warrant.Wildcard || val == target { - return true + if nextCursor == nil { + return result, nil } - } - return false + listParams.NextCursor = nextCursor + } } func objectKey(objectType string, objectId string) string { diff --git a/pkg/authz/query/spec.go b/pkg/authz/query/spec.go index 2149f4dd..e35b0a0c 100644 --- a/pkg/authz/query/spec.go +++ b/pkg/authz/query/spec.go @@ -15,6 +15,9 @@ package authz import ( + "fmt" + "strings" + baseWarrant "github.com/warrant-dev/warrant/pkg/authz/warrant" "github.com/warrant-dev/warrant/pkg/service" ) @@ -23,12 +26,25 @@ type Query struct { Expand bool SelectSubjects *SelectSubjects SelectObjects *SelectObjects - Context *baseWarrant.PolicyContext + Context baseWarrant.PolicyContext rawString string } func (q Query) String() string { - return q.rawString + var str string + if q.Expand { + str = "select" + } else { + str = "select explicit" + } + + if q.SelectObjects != nil { + return fmt.Sprintf("%s %s %s", str, q.SelectObjects.String(), q.Context.String()) + } else if q.SelectSubjects != nil { + return fmt.Sprintf("%s %s %s", str, q.SelectSubjects.String(), q.Context.String()) + } + + return "" } type SelectSubjects struct { @@ -37,17 +53,39 @@ type SelectSubjects struct { SubjectTypes []string } +func (s SelectSubjects) String() string { + str := fmt.Sprintf("%s of type %s", strings.Join(s.Relations, ", "), strings.Join(s.SubjectTypes, ", ")) + if s.ForObject != nil { + str = fmt.Sprintf("%s for %s", str, s.ForObject.String()) + } + + return str +} + type SelectObjects struct { ObjectTypes []string Relations []string WhereSubject *Resource } +func (s SelectObjects) String() string { + str := strings.Join(s.ObjectTypes, ", ") + if s.WhereSubject != nil { + str = fmt.Sprintf("%s where %s", str, s.WhereSubject.String()) + } + + return fmt.Sprintf("%s is %s", str, strings.Join(s.Relations, ", ")) +} + type Resource struct { Type string Id string } +func (res Resource) String() string { + return fmt.Sprintf("%s:%s", res.Type, res.Id) +} + type QueryHaving struct { ObjectType string `json:"objectType,omitempty"` ObjectId string `json:"objectId,omitempty"` diff --git a/pkg/authz/warrant/handlers.go b/pkg/authz/warrant/handlers.go index 3a4a492c..92a80147 100644 --- a/pkg/authz/warrant/handlers.go +++ b/pkg/authz/warrant/handlers.go @@ -16,7 +16,6 @@ package authz import ( "net/http" - "strings" "github.com/warrant-dev/warrant/pkg/service" ) @@ -153,27 +152,27 @@ func buildFilterOptions(r *http.Request) *FilterParams { queryParams := r.URL.Query() if queryParams.Has("objectType") { - filterOptions.ObjectType = strings.Split(queryParams.Get("objectType"), ",") + filterOptions.ObjectType = queryParams.Get("objectType") } if queryParams.Has("objectId") { - filterOptions.ObjectId = strings.Split(queryParams.Get("objectId"), ",") + filterOptions.ObjectId = queryParams.Get("objectId") } if queryParams.Has("relation") { - filterOptions.Relation = strings.Split(queryParams.Get("relation"), ",") + filterOptions.Relation = queryParams.Get("relation") } if queryParams.Has("subjectType") { - filterOptions.SubjectType = strings.Split(queryParams.Get("subjectType"), ",") + filterOptions.SubjectType = queryParams.Get("subjectType") } if queryParams.Has("subjectId") { - filterOptions.SubjectId = strings.Split(queryParams.Get("subjectId"), ",") + filterOptions.SubjectId = queryParams.Get("subjectId") } if queryParams.Has("subjectRelation") { - filterOptions.SubjectRelation = strings.Split(queryParams.Get("subjectRelation"), ",") + filterOptions.SubjectRelation = queryParams.Get("subjectRelation") } if queryParams.Has("policy") { diff --git a/pkg/authz/warrant/list.go b/pkg/authz/warrant/list.go index 57f5b8bc..e9c59f6e 100644 --- a/pkg/authz/warrant/list.go +++ b/pkg/authz/warrant/list.go @@ -23,39 +23,39 @@ import ( ) type FilterParams struct { - ObjectType []string `json:"objectType,omitempty"` - ObjectId []string `json:"objectId,omitempty"` - Relation []string `json:"relation,omitempty"` - SubjectType []string `json:"subjectType,omitempty"` - SubjectId []string `json:"subjectId,omitempty"` - SubjectRelation []string `json:"subjectRelation,omitempty"` - Policy Policy `json:"policy,omitempty"` + ObjectType string `json:"objectType,omitempty"` + ObjectId string `json:"objectId,omitempty"` + Relation string `json:"relation,omitempty"` + SubjectType string `json:"subjectType,omitempty"` + SubjectId string `json:"subjectId,omitempty"` + SubjectRelation string `json:"subjectRelation,omitempty"` + Policy Policy `json:"policy,omitempty"` } func (fp FilterParams) String() string { s := "" if len(fp.ObjectType) > 0 { - s = fmt.Sprintf("%s&objectType=%s", s, strings.Join(fp.ObjectType, ",")) + s = fmt.Sprintf("%s&objectType=%s", s, fp.ObjectType) } if len(fp.ObjectId) > 0 { - s = fmt.Sprintf("%s&objectId=%s", s, strings.Join(fp.ObjectId, ",")) + s = fmt.Sprintf("%s&objectId=%s", s, fp.ObjectId) } if len(fp.Relation) > 0 { - s = fmt.Sprintf("%s&relation=%s", s, strings.Join(fp.Relation, ",")) + s = fmt.Sprintf("%s&relation=%s", s, fp.Relation) } if len(fp.SubjectType) > 0 { - s = fmt.Sprintf("%s&subjectType=%s", s, strings.Join(fp.SubjectType, ",")) + s = fmt.Sprintf("%s&subjectType=%s", s, fp.SubjectType) } if len(fp.SubjectId) > 0 { - s = fmt.Sprintf("%s&subjectId=%s", s, strings.Join(fp.SubjectId, ",")) + s = fmt.Sprintf("%s&subjectId=%s", s, fp.SubjectId) } if len(fp.SubjectRelation) > 0 { - s = fmt.Sprintf("%s&subjectRelation=%s", s, strings.Join(fp.SubjectRelation, ",")) + s = fmt.Sprintf("%s&subjectRelation=%s", s, fp.SubjectRelation) } if fp.Policy != "" { diff --git a/pkg/authz/warrant/mysql.go b/pkg/authz/warrant/mysql.go index 36e5e769..1d5ef71d 100644 --- a/pkg/authz/warrant/mysql.go +++ b/pkg/authz/warrant/mysql.go @@ -196,55 +196,37 @@ func (repo MySQLRepository) List(ctx context.Context, filterParams FilterParams, WHERE deletedAt IS NULL ` - replacements := []interface{}{} + var replacements []interface{} sortByColumn := listParams.SortBy - if len(filterParams.ObjectType) > 0 { - query = fmt.Sprintf("%s AND objectType IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectType)+1)) - for _, objectType := range filterParams.ObjectType { - replacements = append(replacements, objectType) - } - replacements = append(replacements, Wildcard) + if filterParams.ObjectType != "" { + query = fmt.Sprintf("%s AND objectType = ?", query) + replacements = append(replacements, filterParams.ObjectType) } - if len(filterParams.ObjectId) > 0 { - query = fmt.Sprintf("%s AND objectId IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectId)+1)) - for _, objectId := range filterParams.ObjectId { - replacements = append(replacements, objectId) - } - replacements = append(replacements, Wildcard) + if filterParams.ObjectId != "" && filterParams.ObjectId != Wildcard { + query = fmt.Sprintf("%s AND objectId IN (?, '*')", query) + replacements = append(replacements, filterParams.ObjectId) } - if len(filterParams.Relation) > 0 { - query = fmt.Sprintf("%s AND relation IN (%s)", query, BuildQuestionMarkString(len(filterParams.Relation)+1)) - for _, relation := range filterParams.Relation { - replacements = append(replacements, relation) - } - replacements = append(replacements, Wildcard) + if filterParams.Relation != "" { + query = fmt.Sprintf("%s AND relation = ?", query) + replacements = append(replacements, filterParams.Relation) } - if len(filterParams.SubjectType) > 0 { - query = fmt.Sprintf("%s AND subjectType IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectType)+1)) - for _, subjectType := range filterParams.SubjectType { - replacements = append(replacements, subjectType) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectType != "" { + query = fmt.Sprintf("%s AND subjectType = ?", query) + replacements = append(replacements, filterParams.SubjectType) } - if len(filterParams.SubjectId) > 0 { - query = fmt.Sprintf("%s AND subjectId IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectId)+1)) - for _, subjectId := range filterParams.SubjectId { - replacements = append(replacements, subjectId) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectId != "" { + query = fmt.Sprintf("%s AND subjectId IN (?, '*')", query) + replacements = append(replacements, filterParams.SubjectId) } - if len(filterParams.SubjectRelation) > 0 { - query = fmt.Sprintf("%s AND subjectRelation IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectRelation)+1)) - for _, subjectRelation := range filterParams.SubjectRelation { - replacements = append(replacements, subjectRelation) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectRelation != "" { + query = fmt.Sprintf("%s AND subjectRelation = ?", query) + replacements = append(replacements, filterParams.SubjectRelation) } if filterParams.Policy != "" { diff --git a/pkg/authz/warrant/postgres.go b/pkg/authz/warrant/postgres.go index 5e2b9359..352c3ea2 100644 --- a/pkg/authz/warrant/postgres.go +++ b/pkg/authz/warrant/postgres.go @@ -200,56 +200,38 @@ func (repo PostgresRepository) List(ctx context.Context, filterParams FilterPara WHERE deleted_at IS NULL ` - replacements := []interface{}{} + var replacements []interface{} primarySortKeyColumn := sortRegexp.ReplaceAllString(PrimarySortKey, `_$1`) sortByColumn := sortRegexp.ReplaceAllString(listParams.SortBy, `_$1`) - if len(filterParams.ObjectType) > 0 { - query = fmt.Sprintf("%s AND object_type IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectType)+1)) - for _, objectType := range filterParams.ObjectType { - replacements = append(replacements, objectType) - } - replacements = append(replacements, Wildcard) + if filterParams.ObjectType != "" { + query = fmt.Sprintf("%s AND object_type = ?", query) + replacements = append(replacements, filterParams.ObjectType) } - if len(filterParams.ObjectId) > 0 { - query = fmt.Sprintf("%s AND object_id IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectId)+1)) - for _, objectId := range filterParams.ObjectId { - replacements = append(replacements, objectId) - } - replacements = append(replacements, Wildcard) + if filterParams.ObjectId != "" && filterParams.ObjectId != Wildcard { + query = fmt.Sprintf("%s AND object_id IN (?, '*')", query) + replacements = append(replacements, filterParams.ObjectId) } - if len(filterParams.Relation) > 0 { - query = fmt.Sprintf("%s AND relation IN (%s)", query, BuildQuestionMarkString(len(filterParams.Relation)+1)) - for _, relation := range filterParams.Relation { - replacements = append(replacements, relation) - } - replacements = append(replacements, Wildcard) + if filterParams.Relation != "" { + query = fmt.Sprintf("%s AND relation = ?", query) + replacements = append(replacements, filterParams.Relation) } - if len(filterParams.SubjectType) > 0 { - query = fmt.Sprintf("%s AND subject_type IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectType)+1)) - for _, subjectType := range filterParams.SubjectType { - replacements = append(replacements, subjectType) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectType != "" { + query = fmt.Sprintf("%s AND subject_type = ?", query) + replacements = append(replacements, filterParams.SubjectType) } - if len(filterParams.SubjectId) > 0 { - query = fmt.Sprintf("%s AND subject_id IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectId)+1)) - for _, subjectId := range filterParams.SubjectId { - replacements = append(replacements, subjectId) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectId != "" { + query = fmt.Sprintf("%s AND subject_id IN (?, '*')", query) + replacements = append(replacements, filterParams.SubjectId) } - if len(filterParams.SubjectRelation) > 0 { - query = fmt.Sprintf("%s AND subject_relation IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectRelation)+1)) - for _, subjectRelation := range filterParams.SubjectRelation { - replacements = append(replacements, subjectRelation) - } - replacements = append(replacements, Wildcard) + if filterParams.SubjectRelation != "" { + query = fmt.Sprintf("%s AND subject_relation = ?", query) + replacements = append(replacements, filterParams.SubjectRelation) } if filterParams.Policy != "" { diff --git a/pkg/authz/warrant/sqlite.go b/pkg/authz/warrant/sqlite.go index 3cdcdfbf..f03271c8 100644 --- a/pkg/authz/warrant/sqlite.go +++ b/pkg/authz/warrant/sqlite.go @@ -205,55 +205,37 @@ func (repo SQLiteRepository) List(ctx context.Context, filterParams FilterParams WHERE deletedAt IS NULL ` - replacements := []interface{}{} + var replacements []interface{} sortByColumn := listParams.SortBy if len(filterParams.ObjectType) > 0 { - query = fmt.Sprintf("%s AND objectType IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectType)+1)) - for _, objectType := range filterParams.ObjectType { - replacements = append(replacements, objectType) - } - replacements = append(replacements, Wildcard) + query = fmt.Sprintf("%s AND objectType = ?", query) + replacements = append(replacements, filterParams.ObjectType) } - if len(filterParams.ObjectId) > 0 { - query = fmt.Sprintf("%s AND objectId IN (%s)", query, BuildQuestionMarkString(len(filterParams.ObjectId)+1)) - for _, objectId := range filterParams.ObjectId { - replacements = append(replacements, objectId) - } - replacements = append(replacements, Wildcard) + if len(filterParams.ObjectId) > 0 && filterParams.ObjectId != Wildcard { + query = fmt.Sprintf("%s AND objectId IN (?, '*')", query) + replacements = append(replacements, filterParams.ObjectId) } if len(filterParams.Relation) > 0 { - query = fmt.Sprintf("%s AND relation IN (%s)", query, BuildQuestionMarkString(len(filterParams.Relation)+1)) - for _, relation := range filterParams.Relation { - replacements = append(replacements, relation) - } - replacements = append(replacements, Wildcard) + query = fmt.Sprintf("%s AND relation = ?", query) + replacements = append(replacements, filterParams.Relation) } if len(filterParams.SubjectType) > 0 { - query = fmt.Sprintf("%s AND subjectType IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectType)+1)) - for _, subjectType := range filterParams.SubjectType { - replacements = append(replacements, subjectType) - } - replacements = append(replacements, Wildcard) + query = fmt.Sprintf("%s AND subjectType = ?", query) + replacements = append(replacements, filterParams.SubjectType) } if len(filterParams.SubjectId) > 0 { - query = fmt.Sprintf("%s AND subjectId IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectId)+1)) - for _, subjectId := range filterParams.SubjectId { - replacements = append(replacements, subjectId) - } - replacements = append(replacements, Wildcard) + query = fmt.Sprintf("%s AND subjectId IN (?, '*')", query) + replacements = append(replacements, filterParams.SubjectId) } if len(filterParams.SubjectRelation) > 0 { - query = fmt.Sprintf("%s AND subjectRelation IN (%s)", query, BuildQuestionMarkString(len(filterParams.SubjectRelation)+1)) - for _, subjectRelation := range filterParams.SubjectRelation { - replacements = append(replacements, subjectRelation) - } - replacements = append(replacements, Wildcard) + query = fmt.Sprintf("%s AND subjectRelation = ?", query) + replacements = append(replacements, filterParams.SubjectRelation) } if filterParams.Policy != "" { diff --git a/pkg/service/list.go b/pkg/service/list.go index b58229df..719f006d 100644 --- a/pkg/service/list.go +++ b/pkg/service/list.go @@ -161,7 +161,35 @@ type ListParams struct { defaultSortBy string } -func (lp ListParams) String() string { +func (lp *ListParams) WithPage(page int) { + lp.Page = page +} + +func (lp *ListParams) WithLimit(limit int) { + lp.Limit = limit +} + +func (lp *ListParams) WithQuery(query *string) { + lp.Query = query +} + +func (lp *ListParams) WithSortBy(sortBy string) { + lp.SortBy = sortBy +} + +func (lp *ListParams) WithSortOrder(sortOrder SortOrder) { + lp.SortOrder = sortOrder +} + +func (lp *ListParams) WithPrevCursor(prevCursor *Cursor) { + lp.PrevCursor = prevCursor +} + +func (lp *ListParams) WithNextCursor(nextCursor *Cursor) { + lp.NextCursor = nextCursor +} + +func (lp *ListParams) String() string { s := fmt.Sprintf("page=%d&limit=%d&sortBy=%s&sortOrder=%s&defaultSortBy=%s", lp.Page, lp.Limit, @@ -183,11 +211,11 @@ func (lp ListParams) String() string { return s } -func (lp ListParams) DefaultSortBy() string { +func (lp *ListParams) DefaultSortBy() string { return lp.defaultSortBy } -func (lp ListParams) UseCursorPagination() bool { +func (lp *ListParams) UseCursorPagination() bool { return lp.NextCursor != nil || lp.PrevCursor != nil } @@ -301,7 +329,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { if urlQueryParams.Has(paramNameQuery) { query := urlQueryParams.Get(paramNameQuery) - listParams.Query = &query + listParams.WithQuery(&query) } if urlQueryParams.Has(paramNamePage) { @@ -310,7 +338,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNamePage, err.Error())) return } - listParams.Page = page + listParams.WithPage(page) } if urlQueryParams.Has(paramNameLimit) { @@ -319,7 +347,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNameLimit, err.Error())) return } - listParams.Limit = limit + listParams.WithLimit(limit) } if urlQueryParams.Has(paramNameSortOrder) { @@ -328,7 +356,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNameSortOrder, err.Error())) return } - listParams.SortOrder = sortOrder + listParams.WithSortOrder(sortOrder) } var sortBy string @@ -338,7 +366,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNameSortBy, err.Error())) return } - listParams.SortBy = sortBy + listParams.WithSortBy(sortBy) } if urlQueryParams.Has(paramNameAfterValue) && !urlQueryParams.Has(paramNameAfterId) { @@ -380,7 +408,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { nextCursor = NewCursor(afterId, nil) } - listParams.NextCursor = nextCursor + listParams.WithNextCursor(nextCursor) } if urlQueryParams.Has(paramNameBeforeId) { @@ -402,7 +430,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { prevCursor = NewCursor(beforeId, nil) } - listParams.PrevCursor = prevCursor + listParams.WithPrevCursor(prevCursor) } if urlQueryParams.Has(paramNameNextCursor) && urlQueryParams.Has(paramNamePrevCursor) { @@ -416,7 +444,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNameNextCursor, err.Error())) return } - listParams.NextCursor = nextCursor + listParams.WithNextCursor(nextCursor) } if urlQueryParams.Has(paramNamePrevCursor) { @@ -425,7 +453,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { SendErrorResponse(w, NewInvalidParameterError(paramNamePrevCursor, err.Error())) return } - listParams.PrevCursor = prevCursor + listParams.WithPrevCursor(prevCursor) } ctx := context.WithValue(r.Context(), contextKeyListParams, &listParams) diff --git a/tests/v1/query.json b/tests/v1/query.json index 2c915d75..934cb442 100644 --- a/tests/v1/query.json +++ b/tests/v1/query.json @@ -801,6 +801,21 @@ } }, "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false } ] } @@ -865,12 +880,13 @@ "objectType": "user", "objectId": "U4", "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", + "objectType": "document", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "user", - "objectId": "U4" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -924,12 +940,13 @@ "objectType": "user", "objectId": "U4", "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", + "objectType": "document", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "user", - "objectId": "U4" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -950,7 +967,147 @@ "expectedResponse": { "statusCode": 200, "body": { - "results": [] + "results": [ + { + "objectType": "document", + "objectId": "D1", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D2", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D3", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D6", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false, + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + }, + { + "objectType": "document", + "objectId": "F1", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "F2", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + } + ] } } }, @@ -972,11 +1129,12 @@ "objectId": "D1", "warrant": { "objectType": "document", - "objectId": "D1", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -986,11 +1144,12 @@ "objectId": "D2", "warrant": { "objectType": "document", - "objectId": "D2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1000,11 +1159,12 @@ "objectId": "D3", "warrant": { "objectType": "document", - "objectId": "D3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1014,11 +1174,12 @@ "objectId": "D4", "warrant": { "objectType": "document", - "objectId": "D4", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1028,11 +1189,12 @@ "objectId": "D5", "warrant": { "objectType": "document", - "objectId": "D5", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1076,11 +1238,12 @@ "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1090,11 +1253,12 @@ "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1121,11 +1285,12 @@ "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1135,11 +1300,12 @@ "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1201,11 +1367,12 @@ "objectId": "D5", "warrant": { "objectType": "document", - "objectId": "D5", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1215,11 +1382,12 @@ "objectId": "D4", "warrant": { "objectType": "document", - "objectId": "D4", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1247,11 +1415,12 @@ "objectId": "D3", "warrant": { "objectType": "document", - "objectId": "D3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1261,11 +1430,12 @@ "objectId": "D2", "warrant": { "objectType": "document", - "objectId": "D2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1275,11 +1445,12 @@ "objectId": "D1", "warrant": { "objectType": "document", - "objectId": "D1", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1289,10 +1460,10 @@ } }, { - "name": "selectMembersOrViewersOfTypeUserLimit3SortOrderDesc", + "name": "selectMembersOrViewersOfTypeUserForDocumentD5Limit3SortOrderDesc", "request": { "method": "GET", - "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortOrder=DESC", + "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user%20for%20document:D5&limit=3&sortOrder=DESC", "headers": { "Warrant-Token": "latest" } @@ -1305,12 +1476,13 @@ "objectType": "user", "objectId": "U4", "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", + "objectType": "document", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "user", - "objectId": "U4" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1327,7 +1499,7 @@ "objectId": "U3" } }, - "isImplicit": false + "isImplicit": true }, { "objectType": "user", @@ -1344,7 +1516,7 @@ "isImplicit": true } ], - "lastId": "{{ selectMembersOrViewersOfTypeUserLimit3SortOrderDesc.lastId }}" + "lastId": "{{ selectMembersOrViewersOfTypeUserForDocumentD5Limit3SortOrderDesc.lastId }}" } } }, @@ -1352,7 +1524,7 @@ "name": "selectMembersOrViewersOfTypeUserLimit3SortOrderDescAfterId1", "request": { "method": "GET", - "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortOrder=DESC&lastId={{ selectMembersOrViewersOfTypeUserLimit3SortOrderDesc.lastId }}", + "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user%20for%20document:D5&limit=3&sortOrder=DESC&lastId={{ selectMembersOrViewersOfTypeUserForDocumentD5Limit3SortOrderDesc.lastId }}", "headers": { "Warrant-Token": "latest" } @@ -1510,10 +1682,27 @@ } }, { - "name": "queryFailsWhenNoneOfIsUsed", + "name": "queryFailsWhenNoneOfIsUsedSelectObjects", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U4%20is%20nothing", + "headers": { + "Warrant-Token": "latest" + } + }, + "expectedResponse": { + "statusCode": 400, + "body": { + "code": "invalid_request", + "message": "cannot query authorization models with object types that use the 'noneOf' operator." + } + } + }, + { + "name": "queryFailsWhenNoneOfIsUsedSelectSubjects", "request": { "method": "GET", - "url": "/v1/query?q=select%20%2A", + "url": "/v1/query?q=select%20nothing%20of%20type%20user%20for%20document:D6", "headers": { "Warrant-Token": "latest" } diff --git a/tests/v1/warrants-list.json b/tests/v1/warrants-list.json index ce918475..28522465 100644 --- a/tests/v1/warrants-list.json +++ b/tests/v1/warrants-list.json @@ -182,45 +182,6 @@ ] } }, - { - "name": "listWarrantsFilterByObjectTypePermissionOrReport", - "request": { - "method": "GET", - "url": "/v1/warrants?objectType=permission,report" - }, - "expectedResponse": { - "statusCode": 200, - "body": [ - { - "objectType": "permission", - "objectId": "view-balance-sheet", - "relation": "member", - "subject": { - "objectType": "role", - "objectId": "senior-accountant" - } - }, - { - "objectType": "permission", - "objectId": "balance-sheet:edit", - "relation": "member", - "subject": { - "objectType": "user", - "objectId": "user-b" - } - }, - { - "objectType": "report", - "objectId": "report-a", - "relation": "owner", - "subject": { - "objectType": "user", - "objectId": "user-a" - } - } - ] - } - }, { "name": "listWarrantsFilterByObjectTypePermissionObjectIdViewBalanceSheet", "request": { diff --git a/tests/v2/query.json b/tests/v2/query.json index 70384707..f4cc787c 100644 --- a/tests/v2/query.json +++ b/tests/v2/query.json @@ -801,6 +801,21 @@ } }, "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false } ] } @@ -865,12 +880,13 @@ "objectType": "user", "objectId": "U4", "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", + "objectType": "document", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "user", - "objectId": "U4" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -924,12 +940,13 @@ "objectType": "user", "objectId": "U4", "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", + "objectType": "document", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "user", - "objectId": "U4" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -950,7 +967,147 @@ "expectedResponse": { "statusCode": 200, "body": { - "results": [] + "results": [ + { + "objectType": "document", + "objectId": "D1", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D2", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D3", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "D6", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false, + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + }, + { + "objectType": "document", + "objectId": "F1", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "F2", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + }, + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": false + } + ] } } }, @@ -972,11 +1129,12 @@ "objectId": "D1", "warrant": { "objectType": "document", - "objectId": "D1", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -986,11 +1144,12 @@ "objectId": "D2", "warrant": { "objectType": "document", - "objectId": "D2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1000,11 +1159,12 @@ "objectId": "D3", "warrant": { "objectType": "document", - "objectId": "D3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1014,11 +1174,12 @@ "objectId": "D4", "warrant": { "objectType": "document", - "objectId": "D4", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1028,11 +1189,12 @@ "objectId": "D5", "warrant": { "objectType": "document", - "objectId": "D5", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1076,11 +1238,12 @@ "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1090,11 +1253,12 @@ "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1121,11 +1285,12 @@ "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1135,11 +1300,12 @@ "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1201,11 +1367,12 @@ "objectId": "D5", "warrant": { "objectType": "document", - "objectId": "D5", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1215,11 +1382,12 @@ "objectId": "D4", "warrant": { "objectType": "document", - "objectId": "D4", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F3" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1248,11 +1416,12 @@ "objectId": "D3", "warrant": { "objectType": "document", - "objectId": "D3", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1262,11 +1431,12 @@ "objectId": "D2", "warrant": { "objectType": "document", - "objectId": "D2", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F2" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1276,11 +1446,12 @@ "objectId": "D1", "warrant": { "objectType": "document", - "objectId": "D1", - "relation": "parent", + "objectId": "*", + "relation": "owner", "subject": { - "objectType": "document", - "objectId": "F1" + "objectType": "role", + "objectId": "admin", + "relation": "member" } }, "isImplicit": true @@ -1291,10 +1462,10 @@ } }, { - "name": "selectMembersOrViewersOfTypeUserLimit3SortOrderDesc", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDesc", "request": { "method": "GET", - "url": "/v2/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortOrder=DESC", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=2&sortOrder=DESC", "headers": { "Warrant-Token": "latest" } @@ -1304,57 +1475,43 @@ "body": { "results": [ { - "objectType": "user", - "objectId": "U4", - "warrant": { - "objectType": "role", - "objectId": "admin", - "relation": "member", - "subject": { - "objectType": "user", - "objectId": "U4" - } - }, - "isImplicit": true - }, - { - "objectType": "user", - "objectId": "U3", + "objectType": "document", + "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F3", - "relation": "viewer", + "objectId": "D5", + "relation": "parent", "subject": { - "objectType": "user", - "objectId": "U3" + "objectType": "document", + "objectId": "F3" } }, "isImplicit": false }, { - "objectType": "user", - "objectId": "U2", + "objectType": "document", + "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "editor", + "objectId": "F3", + "relation": "parent", "subject": { - "objectType": "user", - "objectId": "U2" + "objectType": "document", + "objectId": "F2" } }, "isImplicit": true } ], - "nextCursor": "{{ selectMembersOrViewersOfTypeUserLimit3SortOrderDesc.nextCursor }}" + "nextCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDesc.nextCursor }}" } } }, { - "name": "selectMembersOrViewersOfTypeUserLimit3SortOrderDescNextCursor1", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDescNextCursor1", "request": { "method": "GET", - "url": "/v2/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortOrder=DESC&nextCursor={{ selectMembersOrViewersOfTypeUserLimit3SortOrderDesc.nextCursor }}", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=2&sortOrder=DESC&nextCursor={{ selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDesc.nextCursor }}", "headers": { "Warrant-Token": "latest" } @@ -1364,29 +1521,29 @@ "body": { "results": [ { - "objectType": "user", - "objectId": "U1", + "objectType": "document", + "objectId": "F1", "warrant": { "objectType": "document", - "objectId": "F1", - "relation": "owner", + "objectId": "F2", + "relation": "parent", "subject": { - "objectType": "user", - "objectId": "U1" + "objectType": "document", + "objectId": "F1" } }, "isImplicit": true } ], - "prevCursor": "{{ selectMembersOrViewersOfTypeUserLimit3SortOrderDescNextCursor1.prevCursor }}" + "prevCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDescNextCursor1.prevCursor }}" } } }, { - "name": "selectMembersOrViewersOfTypeUserLimit2SortOrderDescPrevCursor1", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit1SortOrderDescPrevCursor1", "request": { "method": "GET", - "url": "/v2/query?q=select%20member%2Cviewer%20of%20type%20user&limit=2&sortOrder=DESC&prevCursor={{ selectMembersOrViewersOfTypeUserLimit3SortOrderDescNextCursor1.prevCursor }}", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=1&sortOrder=DESC&prevCursor={{ selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDescNextCursor1.prevCursor }}", "headers": { "Warrant-Token": "latest" } @@ -1396,44 +1553,62 @@ "body": { "results": [ { - "objectType": "user", - "objectId": "U3", + "objectType": "document", + "objectId": "F2", "warrant": { "objectType": "document", "objectId": "F3", - "relation": "viewer", + "relation": "parent", "subject": { - "objectType": "user", - "objectId": "U3" + "objectType": "document", + "objectId": "F2" } }, - "isImplicit": false - }, + "isImplicit": true + } + ], + "prevCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit1SortOrderDescPrevCursor1.prevCursor }}", + "nextCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit2SortOrderDesc.nextCursor }}" + } + } + }, + { + "name": "selectParentsOfTypeDocumentForDocumentD5Limit1SortOrderDescPrevCursor2", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=1&sortOrder=DESC&prevCursor={{ selectParentsOfTypeDocumentForDocumentD5Limit1SortOrderDescPrevCursor1.prevCursor }}", + "headers": { + "Warrant-Token": "latest" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ { - "objectType": "user", - "objectId": "U2", + "objectType": "document", + "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "F2", - "relation": "editor", + "objectId": "D5", + "relation": "parent", "subject": { - "objectType": "user", - "objectId": "U2" + "objectType": "document", + "objectId": "F3" } }, - "isImplicit": true + "isImplicit": false } ], - "prevCursor": "{{ selectMembersOrViewersOfTypeUserLimit2SortOrderDescPrevCursor1.prevCursor }}", - "nextCursor": "{{ selectMembersOrViewersOfTypeUserLimit3SortOrderDesc.nextCursor }}" + "nextCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit1SortOrderDescPrevCursor2.nextCursor }}" } } }, { - "name": "selectParentsOfTypeDocumentLimit2SortByCreatedAtAsc", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit2SortByCreatedAtAsc", "request": { "method": "GET", - "url": "/v2/query?q=select%20parent%20of%20type%20document&limit=2&sortBy=createdAt&sortOrder=ASC", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=2&sortBy=createdAt&sortOrder=ASC", "headers": { "Warrant-Token": "latest" } @@ -1447,39 +1622,39 @@ "objectId": "F1", "warrant": { "objectType": "document", - "objectId": "D1", + "objectId": "F2", "relation": "parent", "subject": { "objectType": "document", "objectId": "F1" } }, - "isImplicit": false + "isImplicit": true }, { "objectType": "document", "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "D2", + "objectId": "F3", "relation": "parent", "subject": { "objectType": "document", "objectId": "F2" } }, - "isImplicit": false + "isImplicit": true } ], - "nextCursor": "{{ selectParentsOfTypeDocumentLimit2SortByCreatedAtAsc.nextCursor }}" + "nextCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit2SortByCreatedAtAsc.nextCursor }}" } } }, { - "name": "selectParentsOfTypeDocumentLimit2SortByCreatedAtAscNextCursor1", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit1SortByCreatedAtAscNextCursor1", "request": { "method": "GET", - "url": "/v2/query?q=select%20parent%20of%20type%20document&limit=2&sortBy=createdAt&sortOrder=ASC&nextCursor={{ selectParentsOfTypeDocumentLimit2SortByCreatedAtAsc.nextCursor }}", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=1&sortBy=createdAt&sortOrder=ASC&nextCursor={{ selectParentsOfTypeDocumentForDocumentD5Limit2SortByCreatedAtAsc.nextCursor }}", "headers": { "Warrant-Token": "latest" } @@ -1493,7 +1668,7 @@ "objectId": "F3", "warrant": { "objectType": "document", - "objectId": "D4", + "objectId": "D5", "relation": "parent", "subject": { "objectType": "document", @@ -1503,15 +1678,15 @@ "isImplicit": false } ], - "prevCursor": "{{ selectParentsOfTypeDocumentLimit2SortByCreatedAtAscNextCursor1.prevCursor }}" + "prevCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit1SortByCreatedAtAscNextCursor1.prevCursor }}" } } }, { - "name": "selectParentsOfTypeDocumentLimit2SortByCreatedAtAscPrevCursor1", + "name": "selectParentsOfTypeDocumentForDocumentD5Limit2SortByCreatedAtAscPrevCursor1", "request": { "method": "GET", - "url": "/v2/query?q=select%20parent%20of%20type%20document&limit=2&sortBy=createdAt&sortOrder=ASC&prevCursor={{ selectParentsOfTypeDocumentLimit2SortByCreatedAtAscNextCursor1.prevCursor }}", + "url": "/v2/query?q=select%20parent%20of%20type%20document%20for%20document:D5&limit=2&sortBy=createdAt&sortOrder=ASC&prevCursor={{ selectParentsOfTypeDocumentForDocumentD5Limit1SortByCreatedAtAscNextCursor1.prevCursor }}", "headers": { "Warrant-Token": "latest" } @@ -1525,31 +1700,31 @@ "objectId": "F1", "warrant": { "objectType": "document", - "objectId": "D1", + "objectId": "F2", "relation": "parent", "subject": { "objectType": "document", "objectId": "F1" } }, - "isImplicit": false + "isImplicit": true }, { "objectType": "document", "objectId": "F2", "warrant": { "objectType": "document", - "objectId": "D2", + "objectId": "F3", "relation": "parent", "subject": { "objectType": "document", "objectId": "F2" } }, - "isImplicit": false + "isImplicit": true } ], - "nextCursor": "{{ selectParentsOfTypeDocumentLimit2SortByCreatedAtAsc.nextCursor }}" + "nextCursor": "{{ selectParentsOfTypeDocumentForDocumentD5Limit2SortByCreatedAtAsc.nextCursor }}" } } }, @@ -1684,10 +1859,27 @@ } }, { - "name": "queryFailsWhenNoneOfIsUsed", + "name": "queryFailsWhenNoneOfIsUsedSelectObjects", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20document%20where%20user:U4%20is%20nothing", + "headers": { + "Warrant-Token": "latest" + } + }, + "expectedResponse": { + "statusCode": 400, + "body": { + "code": "invalid_request", + "message": "cannot query authorization models with object types that use the 'noneOf' operator." + } + } + }, + { + "name": "queryFailsWhenNoneOfIsUsedSelectSubjects", "request": { "method": "GET", - "url": "/v2/query?q=select%20%2A", + "url": "/v2/query?q=select%20nothing%20of%20type%20user%20for%20document:D6", "headers": { "Warrant-Token": "latest" } @@ -1906,6 +2098,34 @@ } } }, + { + "name": "assignRoleManagerMemberOfPermissionAddUser", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "permission", + "objectId": "add-user", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "permission", + "objectId": "add-user", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + } + } + }, { "name": "assignUserRichardMemberOfRoleDeveloper", "request": { @@ -1934,6 +2154,34 @@ } } }, + { + "name": "assignUserTomMemberOfRoleManager", + "request": { + "method": "POST", + "url": "/v2/warrants", + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "tom" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "tom" + } + } + } + }, { "name": "selectMembersOfTypeUserForPermissionViewDocs", "request": { @@ -1960,6 +2208,20 @@ } }, "isImplicit": true + }, + { + "objectType": "user", + "objectId": "tom", + "warrant": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "tom" + } + }, + "isImplicit": true } ] } @@ -2019,11 +2281,111 @@ } }, "isImplicit": true + }, + { + "objectType": "user", + "objectId": "tom", + "warrant": { + "objectType": "role", + "objectId": "manager", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "tom" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserRichardIsMember", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:richard%20is%20member", + "headers": { + "Warrant-Token": "latest" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "view-docs", + "warrant": { + "objectType": "permission", + "objectId": "view-docs", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "developer" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectPermissionsWhereUserTomIsMember", + "request": { + "method": "GET", + "url": "/v2/query?q=select%20permission%20where%20user:tom%20is%20member", + "headers": { + "Warrant-Token": "latest" + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "permission", + "objectId": "add-user", + "warrant": { + "objectType": "permission", + "objectId": "add-user", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "manager" + } + }, + "isImplicit": true + }, + { + "objectType": "permission", + "objectId": "view-docs", + "warrant": { + "objectType": "permission", + "objectId": "view-docs", + "relation": "member", + "subject": { + "objectType": "role", + "objectId": "developer" + } + }, + "isImplicit": true } ] } } }, + { + "name": "deleteUserTom", + "request": { + "method": "DELETE", + "url": "/v2/objects/user/tom" + }, + "expectedResponse": { + "statusCode": 200 + } + }, { "name": "deleteUserRichard", "request": { @@ -2054,6 +2416,16 @@ "statusCode": 200 } }, + { + "name": "deletePermissionAddUser", + "request": { + "method": "DELETE", + "url": "/v2/objects/permission/add-user" + }, + "expectedResponse": { + "statusCode": 200 + } + }, { "name": "deletePermissionViewDocs", "request": { diff --git a/tests/v2/warrants-list.json b/tests/v2/warrants-list.json index 645d4747..d4444e8b 100644 --- a/tests/v2/warrants-list.json +++ b/tests/v2/warrants-list.json @@ -252,47 +252,6 @@ } } }, - { - "name": "listWarrantsFilterByObjectTypePermissionOrReport", - "request": { - "method": "GET", - "url": "/v2/warrants?objectType=permission,report" - }, - "expectedResponse": { - "statusCode": 200, - "body": { - "results": [ - { - "objectType": "permission", - "objectId": "view-balance-sheet", - "relation": "member", - "subject": { - "objectType": "role", - "objectId": "senior-accountant" - } - }, - { - "objectType": "permission", - "objectId": "balance-sheet:edit", - "relation": "member", - "subject": { - "objectType": "user", - "objectId": "user-b" - } - }, - { - "objectType": "report", - "objectId": "report-a", - "relation": "owner", - "subject": { - "objectType": "user", - "objectId": "user-a" - } - } - ] - } - } - }, { "name": "listWarrantsFilterByObjectTypePermissionObjectIdViewBalanceSheet", "request": {