diff --git a/cmd/warrant/main.go b/cmd/warrant/main.go index 40bed854..346c5b0a 100644 --- a/cmd/warrant/main.go +++ b/cmd/warrant/main.go @@ -23,18 +23,19 @@ import ( "github.com/pkg/errors" "github.com/rs/zerolog/log" check "github.com/warrant-dev/warrant/pkg/authz/check" - feature "github.com/warrant-dev/warrant/pkg/authz/feature" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" - permission "github.com/warrant-dev/warrant/pkg/authz/permission" - pricingtier "github.com/warrant-dev/warrant/pkg/authz/pricingtier" - role "github.com/warrant-dev/warrant/pkg/authz/role" - tenant "github.com/warrant-dev/warrant/pkg/authz/tenant" - user "github.com/warrant-dev/warrant/pkg/authz/user" + query "github.com/warrant-dev/warrant/pkg/authz/query" warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" "github.com/warrant-dev/warrant/pkg/config" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" + feature "github.com/warrant-dev/warrant/pkg/object/feature" + permission "github.com/warrant-dev/warrant/pkg/object/permission" + pricingtier "github.com/warrant-dev/warrant/pkg/object/pricingtier" + role "github.com/warrant-dev/warrant/pkg/object/role" + tenant "github.com/warrant-dev/warrant/pkg/object/tenant" + user "github.com/warrant-dev/warrant/pkg/object/user" "github.com/warrant-dev/warrant/pkg/service" ) @@ -233,6 +234,9 @@ func main() { // Init check service checkSvc := check.NewService(svcEnv, warrantRepository, eventSvc, objectTypeSvc, cfg.Check, nil) + // Init query service + querySvc := query.NewService(svcEnv, objectTypeSvc, warrantSvc, objectSvc) + // Init feature service featureSvc := feature.NewService(&svcEnv, eventSvc, objectSvc) @@ -259,6 +263,7 @@ func main() { objectTypeSvc, permissionSvc, pricingTierSvc, + querySvc, roleSvc, tenantSvc, userSvc, diff --git a/go.mod b/go.mod index f3e857e2..e7448877 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/warrant-dev/warrant go 1.21 require ( + github.com/alecthomas/participle/v2 v2.1.0 github.com/antonmedv/expr v1.15.3 github.com/go-playground/validator/v10 v10.15.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index f8300210..da092ff9 100644 --- a/go.sum +++ b/go.sum @@ -42,6 +42,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.0 h1:z7dElHRrOEEq45F2TG5cbQihMtNTv8vwldytDj7Wrz4= +github.com/alecthomas/participle/v2 v2.1.0/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= +github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/antonmedv/expr v1.15.3 h1:q3hOJZNvLvhqE8OHBs1cFRdbXFNKuA+bHmRaI+AmRmI= github.com/antonmedv/expr v1.15.3/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -177,6 +183,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= diff --git a/pkg/authz/query/handlers.go b/pkg/authz/query/handlers.go new file mode 100644 index 00000000..a4814427 --- /dev/null +++ b/pkg/authz/query/handlers.go @@ -0,0 +1,56 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "net/http" + + "github.com/warrant-dev/warrant/pkg/service" +) + +func (svc QueryService) Routes() ([]service.Route, error) { + return []service.Route{ + service.WarrantRoute{ + Pattern: "/v1/query", + Method: "GET", + Handler: service.ChainMiddleware( + service.NewRouteHandler(svc, QueryHandler), + service.ListMiddleware[QueryListParamParser], + ), + }, + }, nil +} + +func QueryHandler(svc QueryService, w http.ResponseWriter, r *http.Request) error { + queryString := r.URL.Query().Get("q") + query, err := NewQueryFromString(queryString) + if err != nil { + return err + } + + listParams := service.GetListParamsFromContext[QueryListParamParser](r.Context()) + lastId := r.URL.Query().Get("lastId") + if lastId != "" && listParams.AfterId == nil { + listParams.AfterId = &lastId + } + + result, err := svc.Query(r.Context(), query, listParams) + if err != nil { + return err + } + + service.SendJSONResponse(w, result) + return nil +} diff --git a/pkg/authz/query/list.go b/pkg/authz/query/list.go new file mode 100644 index 00000000..05fa3d09 --- /dev/null +++ b/pkg/authz/query/list.go @@ -0,0 +1,33 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "fmt" +) + +type QueryListParamParser struct{} + +func (parser QueryListParamParser) GetDefaultSortBy() string { + return "objectType" +} + +func (parser QueryListParamParser) GetSupportedSortBys() []string { + return []string{"objectType", "objectId"} +} + +func (parser QueryListParamParser) ParseValue(val string, sortBy string) (interface{}, error) { + return nil, fmt.Errorf("must match type of selected sortBy attribute %s", sortBy) +} diff --git a/pkg/authz/query/parser.go b/pkg/authz/query/parser.go new file mode 100644 index 00000000..c3691a9f --- /dev/null +++ b/pkg/authz/query/parser.go @@ -0,0 +1,176 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "strings" + + "github.com/alecthomas/participle/v2" + "github.com/alecthomas/participle/v2/lexer" + "github.com/pkg/errors" + warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" + "github.com/warrant-dev/warrant/pkg/service" +) + +type selectClause struct { + Explicit bool `parser:"@Explicit?"` + ObjectTypesOrRelations []string `parser:"(@Wildcard | (@TypeOrRelation (Comma @TypeOrRelation)*))?"` + SubjectTypes []string `parser:"(OfType (@Wildcard | (@TypeOrRelation (Comma @TypeOrRelation)*)))?"` +} + +type forClause struct { + Object string `parser:"@Resource"` +} + +type whereClause struct { + Subject string `parser:"@Resource Is"` + Relations []string `parser:"(@Wildcard | (@TypeOrRelation (Comma @TypeOrRelation)*))"` +} + +type ast struct { + SelectClause *selectClause `parser:"Select @@"` + ForClause *forClause `parser:"(For @@)?"` + WhereClause *whereClause `parser:"(Where @@)?"` +} + +var participleLexer = lexer.MustSimple([]lexer.SimpleRule{ + {Name: "Select", Pattern: `(?i)select`}, + {Name: "Explicit", Pattern: `(?i)explicit`}, + {Name: "Where", Pattern: `(?i)where`}, + {Name: "Is", Pattern: `(?i)is`}, + {Name: "For", Pattern: `(?i)for`}, + {Name: "OfType", Pattern: `(?i)of type`}, + {Name: "Resource", Pattern: `[a-zA-Z0-9_\-]+:[a-zA-Z0-9_\-\.@\|:]+`}, + {Name: "TypeOrRelation", Pattern: `[a-zA-Z0-9_\-]+`}, + {Name: "Wildcard", Pattern: `\*`}, + {Name: "Comma", Pattern: `,`}, + {Name: "EOL", Pattern: `[\n\r]+`}, + {Name: "whitespace", Pattern: `[ \t\n\r]+`}, +}) + +type parser struct { + *participle.Parser[ast] +} + +func newParser() (*parser, error) { + participleParser, err := participle.Build[ast]( + participle.Lexer(participleLexer), + ) + if err != nil { + return nil, errors.Wrap(err, "error generating query parser") + } + + return &parser{participleParser}, nil +} + +func (parser parser) Parse(query string) (*ast, error) { + ast, err := parser.Parser.ParseString("", query) + if err != nil { + return nil, errors.Wrap(err, "error parsing query") + } + + return ast, nil +} + +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") + } + + ast, err := queryParser.Parse(queryString) + if err != nil { + return nil, service.NewInvalidParameterError("q", err.Error()) + } + + if ast.SelectClause == nil { + return nil, 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") + } + + if ast.ForClause != nil && ast.WhereClause != nil { + return nil, 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") + } + + 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") + } + + if ast.WhereClause != nil { + return nil, service.NewInvalidParameterError("q", "cannot contain a 'where' clause when selecting subjects") + } + + query.SelectSubjects = &SelectSubjects{ + Relations: ast.SelectClause.ObjectTypesOrRelations, + 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, + } + } + } 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") + } + + if ast.ForClause != nil { + return nil, service.NewInvalidParameterError("q", "cannot contain a 'for' clause when selecting objects") + } + + query.SelectObjects = &SelectObjects{ + ObjectTypes: ast.SelectClause.ObjectTypesOrRelations, + 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, + } + } + } + + return &query, nil +} diff --git a/pkg/authz/query/resultset.go b/pkg/authz/query/resultset.go new file mode 100644 index 00000000..71bf27ea --- /dev/null +++ b/pkg/authz/query/resultset.go @@ -0,0 +1,126 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "fmt" + "strings" + + warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" +) + +type ResultSetNode struct { + ObjectType string + ObjectId string + Warrant warrant.WarrantSpec + next *ResultSetNode +} + +func (node ResultSetNode) Next() *ResultSetNode { + return node.next +} + +type ResultSet struct { + m map[string]*ResultSetNode + head *ResultSetNode + tail *ResultSetNode +} + +func (rs ResultSet) List() *ResultSetNode { + return rs.head +} + +func (rs *ResultSet) Add(objectType string, objectId string, warrant warrant.WarrantSpec) { + if _, exists := rs.m[key(objectType, objectId)]; !exists { + // Add warrant to list + newNode := &ResultSetNode{ + ObjectType: objectType, + ObjectId: objectId, + Warrant: warrant, + next: nil, + } + + if rs.head == nil { + rs.head = newNode + } + + if rs.tail != nil { + rs.tail.next = newNode + } + + rs.tail = newNode + + // Add result node to map for O(1) lookups + rs.m[key(objectType, objectId)] = newNode + } +} + +func (rs ResultSet) Len() int { + return len(rs.m) +} + +func (rs ResultSet) Get(objectType string, objectId string) *ResultSetNode { + return rs.m[key(objectType, objectId)] +} + +func (rs ResultSet) Has(objectType string, objectId string) bool { + _, exists := rs.m[key(objectType, objectId)] + return exists +} + +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.Warrant) + } + + for iter := other.List(); iter != nil; iter = iter.Next() { + resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Warrant) + } + + return resultSet +} + +func (rs ResultSet) Intersect(other *ResultSet) *ResultSet { + resultSet := NewResultSet() + for iter := rs.List(); iter != nil; iter = iter.Next() { + if other.Has(iter.ObjectType, iter.ObjectId) { + resultSet.Add(iter.ObjectType, iter.ObjectId, iter.Warrant) + } + } + + return resultSet +} + +func (rs ResultSet) String() string { + var strs []string + for iter := rs.List(); iter != nil; iter = iter.Next() { + strs = append(strs, fmt.Sprintf("%s => %s", key(iter.ObjectType, iter.ObjectId), iter.Warrant.String())) + } + + return strings.Join(strs, ", ") +} + +func NewResultSet() *ResultSet { + return &ResultSet{ + m: make(map[string]*ResultSetNode), + head: nil, + tail: nil, + } +} + +func key(objectType string, objectId string) string { + return fmt.Sprintf("%s:%s", objectType, objectId) +} diff --git a/pkg/authz/query/service.go b/pkg/authz/query/service.go new file mode 100644 index 00000000..cacb0edb --- /dev/null +++ b/pkg/authz/query/service.go @@ -0,0 +1,475 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "context" + "errors" + "fmt" + "sort" + + "github.com/rs/zerolog/log" + objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + warrant "github.com/warrant-dev/warrant/pkg/authz/warrant" + object "github.com/warrant-dev/warrant/pkg/object" + "github.com/warrant-dev/warrant/pkg/service" + baseSvc "github.com/warrant-dev/warrant/pkg/service" +) + +var ErrInvalidQuery = errors.New("invalid query") + +type QueryService struct { + baseSvc.BaseService + objectTypeSvc *objecttype.ObjectTypeService + warrantSvc *warrant.WarrantService + objectSvc *object.ObjectService +} + +func NewService(env service.Env, objectTypeSvc *objecttype.ObjectTypeService, warrantSvc *warrant.WarrantService, objectSvc *object.ObjectService) QueryService { + return QueryService{ + BaseService: baseSvc.NewBaseService(env), + objectTypeSvc: objectTypeSvc, + warrantSvc: warrantSvc, + objectSvc: objectSvc, + } +} + +func (svc QueryService) Query(ctx context.Context, query *Query, listParams service.ListParams) (*Result, error) { + queryResults := []QueryResult{} + resultMap := make(map[string]int) + objects := make(map[string][]string, 0) + selectedObjectTypes := make(map[string]bool) + + if (query.SelectObjects == nil && query.SelectSubjects == nil) || (query.SelectObjects != nil && query.SelectSubjects != nil) { + return nil, ErrInvalidQuery + } + + objectTypeMap, err := svc.objectTypeSvc.GetTypeMap(ctx) + if err != nil { + return nil, err + } + + resultSet, err := svc.query(ctx, query, objectTypeMap) + if err != nil { + return nil, err + } + + for res := resultSet.List(); res != nil; res = res.Next() { + var isImplicit bool + //nolint:gocritic + if query.SelectObjects != nil { + isImplicit = !matches(query.SelectObjects.ObjectTypes, res.Warrant.ObjectType) || !matches(query.SelectObjects.Relations, res.Warrant.Relation) + } else if query.SelectSubjects != nil { + isImplicit = !matches(query.SelectSubjects.SubjectTypes, res.Warrant.Subject.ObjectType) || !matches(query.SelectSubjects.Relations, res.Warrant.Relation) + } else { + return nil, ErrInvalidQuery + } + + queryResults = append(queryResults, QueryResult{ + ObjectType: res.ObjectType, + ObjectId: res.ObjectId, + Warrant: res.Warrant, + IsImplicit: isImplicit, + }) + } + + // handle sorting and pagination + switch listParams.SortBy { + case "objectType": + switch listParams.SortOrder { + case service.SortOrderAsc: + sort.Sort(ByObjectTypeAsc(queryResults)) + case service.SortOrderDesc: + sort.Sort(ByObjectTypeDesc(queryResults)) + } + case "objectId": + switch listParams.SortOrder { + case service.SortOrderAsc: + sort.Sort(ByObjectIdAsc(queryResults)) + case service.SortOrderDesc: + sort.Sort(ByObjectIdDesc(queryResults)) + } + default: + return nil, ErrInvalidQuery + } + + index := 0 + // skip ahead if lastId passed in + if listParams.AfterId != nil { + lastIdSpec, err := StringToLastIdSpec(*listParams.AfterId) + if err != nil { + return nil, err + } + + for index < len(queryResults) && (queryResults[index].ObjectType != lastIdSpec.ObjectType || queryResults[index].ObjectId != lastIdSpec.ObjectId) { + index++ + } + } + + paginatedQueryResults := []QueryResult{} + for len(paginatedQueryResults) < listParams.Limit && index < len(queryResults) { + paginatedQueryResult := queryResults[index] + paginatedQueryResults = append(paginatedQueryResults, paginatedQueryResult) + selectedObjectTypes[paginatedQueryResult.ObjectType] = true + objects[paginatedQueryResult.ObjectType] = append(objects[paginatedQueryResult.ObjectType], paginatedQueryResult.ObjectId) + resultMap[objectKey(paginatedQueryResult.ObjectType, paginatedQueryResult.ObjectId)] = len(paginatedQueryResults) - 1 + index++ + } + + lastId := "" + if index < len(queryResults) { + lastId, err = LastIdSpecToString(LastIdSpec{ + ObjectType: queryResults[index].ObjectType, + ObjectId: queryResults[index].ObjectId, + }) + if err != nil { + return nil, err + } + } + + for selectedObjectType := range selectedObjectTypes { + if len(objects[selectedObjectType]) > 0 { + objectSpecs, err := svc.objectSvc.BatchGetByObjectTypeAndIds(ctx, selectedObjectType, objects[selectedObjectType]) + if err != nil { + return nil, err + } + + for _, objectSpec := range objectSpecs { + paginatedQueryResults[resultMap[objectKey(selectedObjectType, objectSpec.ObjectId)]].Meta = objectSpec.Meta + } + } + } + + return &Result{ + Results: paginatedQueryResults, + LastId: lastId, + }, nil +} + +func (svc QueryService) query(ctx context.Context, query *Query, objectTypeMap objecttype.ObjectTypeMap) (*ResultSet, error) { + var objectTypes []string + var selectSubjects bool + //nolint:gocritic + if query.SelectObjects != nil { + selectSubjects = false + if query.SelectObjects.ObjectTypes[0] == warrant.Wildcard { + for objectType := range objectTypeMap { + objectTypes = append(objectTypes, objectType) + } + } 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 objectTypeMap { + objectTypes = append(objectTypes, objectType) + } + } + } else { + return nil, ErrInvalidQuery + } + + resultSet := NewResultSet() + for _, objectType := range objectTypes { + var relations []string + objectTypeDef, err := objectTypeMap.GetByTypeId(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...) + } + } + + 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, objectTypeMap, objectType, relation, matchFilters, query.Expand) + if err != nil { + return nil, err + } + + resultSet = resultSet.Union(res) + } + } + + return resultSet, nil +} + +func (svc QueryService) matchRelation(ctx context.Context, selectSubjects bool, objectTypes objecttype.ObjectTypeMap, objectType string, relation string, matchFilters warrant.FilterParams, expand bool) (*ResultSet, error) { + log.Ctx(ctx).Debug(). + Str("objectType", objectType). + Str("relation", relation). + Str("filters", matchFilters.String()). + Msg("matchRelation") + objectTypeDef, err := objectTypes.GetByTypeId(objectType) + if err != nil { + return nil, err + } + + if _, exists := objectTypeDef.Relations[relation]; !exists { + return NewResultSet(), nil + } + + resultSet := NewResultSet() + // 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 != "" { + res, err := svc.matchRelation(ctx, selectSubjects, objectTypes, matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.Relation, warrant.FilterParams{ + ObjectId: []string{matchedWarrant.Subject.ObjectId}, + SubjectType: matchFilters.SubjectType, + SubjectId: matchFilters.SubjectId, + }, expand) + 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) + } else if len(matchFilters.ObjectId) > 0 { + resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant) + } else { + //nolint:gocritic + if matchedWarrant.ObjectId != warrant.Wildcard { + resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant) + } else if len(matchFilters.ObjectId) > 0 { + resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant) + } 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) + } + } + } + } + } + } else if selectSubjects { + resultSet.Add(matchedWarrant.Subject.ObjectType, matchedWarrant.Subject.ObjectId, matchedWarrant) + } else if matches(matchFilters.SubjectType, matchedWarrant.Subject.ObjectType) && matches(matchFilters.SubjectId, matchedWarrant.Subject.ObjectId) { + resultSet.Add(matchedWarrant.ObjectType, matchedWarrant.ObjectId, matchedWarrant) + } + } + + // explore following levels if requested + if expand { + rule := objectTypeDef.Relations[relation] + res, err := svc.matchRule(ctx, selectSubjects, objectTypes, objectType, relation, &rule, matchFilters, expand) + if err != nil { + return nil, err + } + resultSet = resultSet.Union(res) + } + + return resultSet, nil +} + +func (svc QueryService) matchRule(ctx context.Context, selectSubjects bool, objectTypes objecttype.ObjectTypeMap, objectType string, relation string, rule *objecttype.RelationRule, matchFilters warrant.FilterParams, expand bool) (*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, objectTypes, objectType, relation, rule.InheritIf, rule.Rules, matchFilters, expand) + default: + // inherit relation if subject has: + // (1) InheritIf on this object + if rule.OfType == "" && rule.WithRelation == "" { + return svc.matchRelation(ctx, selectSubjects, objectTypes, objectType, rule.InheritIf, matchFilters, expand) + } + + // 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}, + }) + if err != nil { + return nil, err + } + + resultSet := NewResultSet() + for _, matchedWarrant := range matchedWarrants { + res, err := svc.matchRelation(ctx, selectSubjects, objectTypes, rule.OfType, rule.InheritIf, warrant.FilterParams{ + ObjectType: matchFilters.ObjectType, + ObjectId: []string{matchedWarrant.Subject.ObjectId}, + SubjectType: matchFilters.SubjectType, + SubjectId: matchFilters.SubjectId, + }, expand) + 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) + } else if len(matchFilters.ObjectId) > 0 { + resultSet.Add(matchedWarrant.ObjectType, matchFilters.ObjectId[0], matchedWarrant) + } 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) + } + } + } + } + } + + return resultSet, nil + } +} + +func (svc QueryService) matchSetRule( + ctx context.Context, + selectSubjects bool, + objectTypes objecttype.ObjectTypeMap, + objectType string, + relation string, + setRuleType string, + rules []objecttype.RelationRule, + matchFilters warrant.FilterParams, + expand bool, +) (*ResultSet, error) { + switch setRuleType { + case objecttype.InheritIfAllOf: + var resultSet *ResultSet + for i := range rules { + res, err := svc.matchRule(ctx, selectSubjects, objectTypes, objectType, relation, &rules[i], matchFilters, expand) + 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 { + resultSet = resultSet.Intersect(res) + } + } + + return resultSet, nil + case objecttype.InheritIfAnyOf: + resultSet := NewResultSet() + for i := range rules { + res, err := svc.matchRule(ctx, selectSubjects, objectTypes, objectType, relation, &rules[i], matchFilters, expand) + if err != nil { + return nil, err + } + 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 + } +} + +func (svc QueryService) matchWarrants(ctx context.Context, matchFilters warrant.FilterParams) ([]warrant.WarrantSpec, error) { + warrantListParams := service.DefaultListParams(warrant.WarrantListParamParser{}) + warrantListParams.Limit = 1000 // explore up to 1000 edges + return svc.warrantSvc.List(ctx, &matchFilters, warrantListParams) +} + +func matches(set []string, target string) bool { + if len(set) == 0 || target == warrant.Wildcard { + return true + } + + for _, val := range set { + if val == warrant.Wildcard || val == target { + return true + } + } + + return false +} + +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 new file mode 100644 index 00000000..ead73399 --- /dev/null +++ b/pkg/authz/query/spec.go @@ -0,0 +1,131 @@ +// Copyright 2023 Forerunner Labs, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package authz + +import ( + "encoding/base64" + "encoding/json" + + "github.com/pkg/errors" + baseWarrant "github.com/warrant-dev/warrant/pkg/authz/warrant" +) + +type Query struct { + Expand bool + SelectSubjects *SelectSubjects + SelectObjects *SelectObjects + Context *baseWarrant.PolicyContext +} + +type SelectSubjects struct { + ForObject *Resource + Relations []string + SubjectTypes []string +} + +type SelectObjects struct { + ObjectTypes []string + Relations []string + WhereSubject *Resource +} + +type Resource struct { + Type string + Id string +} + +type QueryHaving 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"` +} + +type QueryResult struct { + ObjectType string `json:"objectType"` + ObjectId string `json:"objectId"` + Warrant baseWarrant.WarrantSpec `json:"warrant"` + IsImplicit bool `json:"isImplicit"` + Meta map[string]interface{} `json:"meta,omitempty"` +} + +type Result struct { + Results []QueryResult `json:"results"` + LastId string `json:"lastId,omitempty"` +} + +type ByObjectTypeAsc []QueryResult + +func (res ByObjectTypeAsc) Len() int { return len(res) } +func (res ByObjectTypeAsc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } +func (res ByObjectTypeAsc) Less(i, j int) bool { + if res[i].ObjectType == res[j].ObjectType { + return res[i].ObjectId < res[j].ObjectId + } + return res[i].ObjectType < res[j].ObjectType +} + +type ByObjectTypeDesc []QueryResult + +func (res ByObjectTypeDesc) Len() int { return len(res) } +func (res ByObjectTypeDesc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } +func (res ByObjectTypeDesc) Less(i, j int) bool { + if res[i].ObjectType == res[j].ObjectType { + return res[i].ObjectId > res[j].ObjectId + } + return res[i].ObjectType > res[j].ObjectType +} + +type ByObjectIdAsc []QueryResult + +func (res ByObjectIdAsc) Len() int { return len(res) } +func (res ByObjectIdAsc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } +func (res ByObjectIdAsc) Less(i, j int) bool { return res[i].ObjectId < res[j].ObjectId } + +type ByObjectIdDesc []QueryResult + +func (res ByObjectIdDesc) Len() int { return len(res) } +func (res ByObjectIdDesc) Swap(i, j int) { res[i], res[j] = res[j], res[i] } +func (res ByObjectIdDesc) Less(i, j int) bool { return res[i].ObjectId > res[j].ObjectId } + +type LastIdSpec struct { + ObjectType string `json:"objectType"` + ObjectId string `json:"objectId"` +} + +func LastIdSpecToString(lastIdSpec LastIdSpec) (string, error) { + jsonStr, err := json.Marshal(lastIdSpec) + if err != nil { + return "", errors.Wrapf(err, "error marshaling lastId %v", lastIdSpec) + } + + return base64.StdEncoding.EncodeToString(jsonStr), nil +} + +func StringToLastIdSpec(base64Str string) (*LastIdSpec, error) { + var lastIdSpec LastIdSpec + jsonStr, err := base64.StdEncoding.DecodeString(base64Str) + if err != nil { + return nil, errors.Wrapf(err, "error base64 decoding lastId string %s", base64Str) + } + + err = json.Unmarshal(jsonStr, &lastIdSpec) + if err != nil { + return nil, errors.Wrapf(err, "error unmarshaling lastIdSpec %v", lastIdSpec) + } + + return &lastIdSpec, nil +} diff --git a/pkg/authz/warrant/handlers.go b/pkg/authz/warrant/handlers.go index 3d603138..43e0780f 100644 --- a/pkg/authz/warrant/handlers.go +++ b/pkg/authz/warrant/handlers.go @@ -16,6 +16,7 @@ package authz import ( "net/http" + "strings" "github.com/warrant-dev/warrant/pkg/service" ) @@ -78,24 +79,11 @@ func CreateHandler(svc WarrantService, w http.ResponseWriter, r *http.Request) e } func ListHandler(svc WarrantService, w http.ResponseWriter, r *http.Request) error { - listParams := service.GetListParamsFromContext[WarrantListParamParser](r.Context()) - queryParams := r.URL.Query() - filters := FilterOptions{ - ObjectType: queryParams.Get("objectType"), - ObjectId: queryParams.Get("objectId"), - Relation: queryParams.Get("relation"), - Subject: &SubjectSpec{ - ObjectType: queryParams.Get("subjectType"), - ObjectId: queryParams.Get("subjectId"), - }, - Policy: Policy(queryParams.Get("policy")), - } - subjectRelation := queryParams.Get("subjectRelation") - if subjectRelation != "" { - filters.Subject.Relation = subjectRelation - } - - warrants, err := svc.List(r.Context(), &filters, listParams) + warrants, err := svc.List( + r.Context(), + buildFilterOptions(r), + service.GetListParamsFromContext[WarrantListParamParser](r.Context()), + ) if err != nil { return err } @@ -120,3 +108,38 @@ func DeleteHandler(svc WarrantService, w http.ResponseWriter, r *http.Request) e w.WriteHeader(http.StatusOK) return nil } + +func buildFilterOptions(r *http.Request) *FilterParams { + var filterOptions FilterParams + queryParams := r.URL.Query() + + if queryParams.Has("objectType") { + filterOptions.ObjectType = strings.Split(queryParams.Get("objectType"), ",") + } + + if queryParams.Has("objectId") { + filterOptions.ObjectId = strings.Split(queryParams.Get("objectId"), ",") + } + + if queryParams.Has("relation") { + filterOptions.Relation = strings.Split(queryParams.Get("relation"), ",") + } + + if queryParams.Has("subjectType") { + filterOptions.SubjectType = strings.Split(queryParams.Get("subjectType"), ",") + } + + if queryParams.Has("subjectId") { + filterOptions.SubjectId = strings.Split(queryParams.Get("subjectId"), ",") + } + + if queryParams.Has("subjectRelation") { + filterOptions.SubjectRelation = strings.Split(queryParams.Get("subjectRelation"), ",") + } + + if queryParams.Has("policy") { + filterOptions.Policy = Policy(queryParams.Get("policy")) + } + + return &filterOptions +} diff --git a/pkg/authz/warrant/list.go b/pkg/authz/warrant/list.go index b45292c6..fee267af 100644 --- a/pkg/authz/warrant/list.go +++ b/pkg/authz/warrant/list.go @@ -16,32 +16,37 @@ package authz import ( "fmt" + "strings" "time" ) -// FilterOptions type for the filter options available on the warrant table -type FilterOptions struct { - ObjectType string - ObjectId string - Relation string - Subject *SubjectSpec - Policy Policy - ObjectIds []string - SubjectIds []string +type FilterParams struct { + ObjectType []string + ObjectId []string + Relation []string + SubjectType []string + SubjectId []string + SubjectRelation []string + Policy Policy } -// SortOptions type for sorting filtered results from the warrant table -type SortOptions struct { - Column string - IsAscending bool +func (fp FilterParams) String() string { + return fmt.Sprintf( + "objectType: '%s' objectId: '%s' relation: '%s' subjectType: '%s' subjectId: '%s' subjectRelation: '%s' policy: '%s'", + strings.Join(fp.ObjectType, ", "), + strings.Join(fp.ObjectId, ", "), + strings.Join(fp.Relation, ", "), + strings.Join(fp.SubjectType, ", "), + strings.Join(fp.SubjectId, ", "), + strings.Join(fp.SubjectRelation, ", "), + fp.Policy, + ) } -const DefaultSortBy = "createdAt" - type WarrantListParamParser struct{} func (parser WarrantListParamParser) GetDefaultSortBy() string { - return DefaultSortBy + return "createdAt" } func (parser WarrantListParamParser) GetSupportedSortBys() []string { diff --git a/pkg/authz/warrant/mysql.go b/pkg/authz/warrant/mysql.go index c8c9a65b..74cc2ddd 100644 --- a/pkg/authz/warrant/mysql.go +++ b/pkg/authz/warrant/mysql.go @@ -292,7 +292,7 @@ func (repo MySQLRepository) GetByID(ctx context.Context, id int64) (Model, error return &warrant, nil } -func (repo MySQLRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { +func (repo MySQLRepository) List(ctx context.Context, filterParams *FilterParams, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) warrants := make([]Warrant, 0) query := ` @@ -302,54 +302,175 @@ func (repo MySQLRepository) List(ctx context.Context, filterOptions *FilterOptio deletedAt IS NULL ` replacements := []interface{}{} + sortByColumn := listParams.SortBy - if filterOptions.ObjectType != "" { - query = fmt.Sprintf("%s AND objectType = ?", query) - replacements = append(replacements, filterOptions.ObjectType) + 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 filterOptions.ObjectId != "" { - query = fmt.Sprintf("%s AND objectId = ?", query) - replacements = append(replacements, filterOptions.ObjectId) + 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 filterOptions.Relation != "" { - query = fmt.Sprintf("%s AND relation = ?", query) - replacements = append(replacements, filterOptions.Relation) + 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 filterOptions.Subject != nil { - if filterOptions.Subject.ObjectType != "" { - query = fmt.Sprintf("%s AND subjectType = ?", query) - - replacements = append(replacements, filterOptions.Subject.ObjectType) + 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 filterOptions.Subject.ObjectId != "" { - query = fmt.Sprintf("%s AND subjectId = ?", query) + 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) + } - replacements = append(replacements, filterOptions.Subject.ObjectId) + 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.Policy != "" { + query = fmt.Sprintf("%s AND policyHash = ?", query) + replacements = append(replacements, filterParams.Policy.Hash()) + } - if filterOptions.Subject.Relation != "" { - query = fmt.Sprintf("%s AND subjectRelation = ?", query) + if listParams.AfterId != nil { + comparisonOp := "<" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = ">" + } - replacements = append(replacements, filterOptions.Subject.Relation) + switch listParams.AfterValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, listParams.DefaultSortBy(), comparisonOp) + replacements = append(replacements, listParams.AfterId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } } } - if filterOptions.Policy != "" { - query = fmt.Sprintf("%s AND policyHash = ?", query) - replacements = append(replacements, filterOptions.Policy.Hash()) + if listParams.BeforeId != nil { + comparisonOp := ">" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = "<" + } + + switch listParams.BeforeValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, listParams.DefaultSortBy(), comparisonOp) + replacements = append(replacements, listParams.BeforeId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } else { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } + } } - if listParams.SortBy != "" { - query = fmt.Sprintf("%s ORDER BY %s %s", query, listParams.SortBy, listParams.SortOrder) + if listParams.BeforeId != nil { + if listParams.SortBy != listParams.DefaultSortBy() { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc, listParams.DefaultSortBy(), service.SortOrderDesc) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc, listParams.DefaultSortBy(), service.SortOrderAsc) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s, %s %s", query, sortByColumn, listParams.SortOrder, listParams.DefaultSortBy(), listParams.SortOrder) + } else { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s", query, sortByColumn, listParams.SortOrder) + } + } else { + if listParams.SortBy != listParams.DefaultSortBy() { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, listParams.SortOrder, listParams.DefaultSortBy(), listParams.SortOrder) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, listParams.DefaultSortBy(), listParams.SortOrder) + replacements = append(replacements, listParams.Limit) + } } - offset := (listParams.Page - 1) * listParams.Limit - query = fmt.Sprintf("%s LIMIT ?, ?", query) - replacements = append(replacements, offset, listParams.Limit) err := repo.DB.SelectContext( ctx, &warrants, @@ -383,7 +504,7 @@ func (repo MySQLRepository) GetAllMatchingObjectRelationAndSubject(ctx context.C FROM warrant WHERE objectType = ? AND - (objectId = ? OR objectId = "*") AND + (objectId = ? OR objectId = ?) AND relation = ? AND subjectType = ? AND subjectId = ? AND @@ -392,6 +513,7 @@ func (repo MySQLRepository) GetAllMatchingObjectRelationAndSubject(ctx context.C `, objectType, objectId, + Wildcard, relation, subjectType, subjectId, @@ -424,13 +546,14 @@ func (repo MySQLRepository) GetAllMatchingObjectAndRelation(ctx context.Context, FROM warrant WHERE objectType = ? AND - (objectId = ? OR objectId = "*") AND + (objectId = ? OR objectId = ?) AND relation = ? AND deletedAt IS NULL ORDER BY createdAt DESC, id DESC `, objectType, objectId, + Wildcard, relation, ) if err != nil { @@ -460,7 +583,7 @@ func (repo MySQLRepository) GetAllMatchingObjectAndRelationBySubjectType(ctx con FROM warrant WHERE objectType = ? AND - (objectId = ? OR objectId = "*") AND + (objectId = ? OR objectId = ?) AND relation = ? AND subjectType = ? AND deletedAt IS NULL @@ -468,6 +591,7 @@ func (repo MySQLRepository) GetAllMatchingObjectAndRelationBySubjectType(ctx con `, objectType, objectId, + Wildcard, relation, subjectType, ) diff --git a/pkg/authz/warrant/postgres.go b/pkg/authz/warrant/postgres.go index ec57eeec..aceaf91a 100644 --- a/pkg/authz/warrant/postgres.go +++ b/pkg/authz/warrant/postgres.go @@ -293,8 +293,7 @@ func (repo PostgresRepository) GetByID(ctx context.Context, id int64) (Model, er return &warrant, nil } -func (repo PostgresRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { - offset := (listParams.Page - 1) * listParams.Limit +func (repo PostgresRepository) List(ctx context.Context, filterParams *FilterParams, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) warrants := make([]Warrant, 0) query := ` @@ -304,54 +303,183 @@ func (repo PostgresRepository) List(ctx context.Context, filterOptions *FilterOp deleted_at IS NULL ` replacements := []interface{}{} + defaultSort := sortRegexp.ReplaceAllString(listParams.DefaultSortBy(), `_$1`) + sortByColumn := sortRegexp.ReplaceAllString(listParams.SortBy, `_$1`) - if filterOptions.ObjectType != "" { - query = fmt.Sprintf(`%s AND object_type = ?`, query) - replacements = append(replacements, filterOptions.ObjectType) + 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 filterOptions.ObjectId != "" { - query = fmt.Sprintf(`%s AND object_id = ?`, query) - replacements = append(replacements, filterOptions.ObjectId) + 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 filterOptions.Relation != "" { - query = fmt.Sprintf(`%s AND relation = ?`, query) - replacements = append(replacements, filterOptions.Relation) + 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 filterOptions.Subject != nil { - if filterOptions.Subject.ObjectType != "" { - query = fmt.Sprintf("%s AND subject_type = ?", query) + 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) + } - replacements = append(replacements, filterOptions.Subject.ObjectType) + 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 filterOptions.Subject.ObjectId != "" { - query = fmt.Sprintf("%s AND subject_id = ?", query) + 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.Policy != "" { + query = fmt.Sprintf("%s AND policy_hash = ?", query) + replacements = append(replacements, filterParams.Policy.Hash()) + } + + if listParams.AfterId != nil { + comparisonOp := "<" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = ">" + } - replacements = append(replacements, filterOptions.Subject.ObjectId) + switch listParams.AfterValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, defaultSort, comparisonOp) + replacements = append(replacements, listParams.AfterId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } } + } - if filterOptions.Subject.Relation != "" { - query = fmt.Sprintf("%s AND subject_relation = ?", query) + if listParams.BeforeId != nil { + comparisonOp := ">" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = "<" + } - replacements = append(replacements, filterOptions.Subject.Relation) + switch listParams.BeforeValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, defaultSort, comparisonOp) + replacements = append(replacements, listParams.BeforeId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } else { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, defaultSort, comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } } } - if filterOptions.Policy != "" { - query = fmt.Sprintf("%s AND policy_hash = ?", query) - replacements = append(replacements, filterOptions.Policy.Hash()) + nullSortClause := "NULLS LAST" + invertedNullSortClause := "NULLS FIRST" + if listParams.SortOrder == service.SortOrderAsc { + nullSortClause = "NULLS FIRST" + invertedNullSortClause = "NULLS LAST" } - if listParams.SortBy != "" { - sortBy := sortRegexp.ReplaceAllString(listParams.SortBy, `_$1`) - query = fmt.Sprintf(`%s ORDER BY %s %s`, query, sortBy, listParams.SortOrder) + if listParams.BeforeId != nil { + if listParams.SortBy != listParams.DefaultSortBy() { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc, invertedNullSortClause, defaultSort, service.SortOrderDesc) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc, invertedNullSortClause, defaultSort, service.SortOrderAsc) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s %s, %s %s", query, sortByColumn, listParams.SortOrder, nullSortClause, defaultSort, listParams.SortOrder) + } else { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc, invertedNullSortClause) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc, invertedNullSortClause) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s %s", query, sortByColumn, listParams.SortOrder, nullSortClause) + } + } else { + if listParams.SortBy != listParams.DefaultSortBy() { + query = fmt.Sprintf("%s ORDER BY %s %s %s, %s %s LIMIT ?", query, sortByColumn, listParams.SortOrder, nullSortClause, defaultSort, listParams.SortOrder) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s %s LIMIT ?", query, defaultSort, listParams.SortOrder, nullSortClause) + replacements = append(replacements, listParams.Limit) + } } - query = fmt.Sprintf(`%s LIMIT ? OFFSET ?`, query) - replacements = append(replacements, listParams.Limit, offset) err := repo.DB.SelectContext( ctx, &warrants, diff --git a/pkg/authz/warrant/repository.go b/pkg/authz/warrant/repository.go index b0985295..27cad5d8 100644 --- a/pkg/authz/warrant/repository.go +++ b/pkg/authz/warrant/repository.go @@ -17,6 +17,7 @@ package authz import ( "context" "fmt" + "strings" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" @@ -30,7 +31,7 @@ type WarrantRepository interface { GetAllMatchingObjectRelationAndSubject(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string) ([]Model, error) GetAllMatchingObjectAndRelation(ctx context.Context, objectType string, objectId string, relation string) ([]Model, error) GetAllMatchingObjectAndRelationBySubjectType(ctx context.Context, objectType string, objectId string, relation string, subjectType string) ([]Model, error) - List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) + List(ctx context.Context, filterParams *FilterParams, listParams service.ListParams) ([]Model, error) Delete(ctx context.Context, objectType string, objectId string, relation string, subjectType string, subjectId string, subjectRelation string, policyHash string) error DeleteById(ctx context.Context, ids []int64) error } @@ -62,3 +63,12 @@ func NewRepository(db database.Database) (WarrantRepository, error) { return nil, errors.New(fmt.Sprintf("unsupported database type %s specified", db.Type())) } } + +func BuildQuestionMarkString(numReplacements int) string { + var replacements []string + for i := 0; i < numReplacements; i++ { + replacements = append(replacements, "?") + } + + return strings.Join(replacements, ",") +} diff --git a/pkg/authz/warrant/service.go b/pkg/authz/warrant/service.go index e398d26a..db38fada 100644 --- a/pkg/authz/warrant/service.go +++ b/pkg/authz/warrant/service.go @@ -18,9 +18,9 @@ import ( "context" "errors" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) @@ -143,15 +143,15 @@ func (svc WarrantService) Create(ctx context.Context, warrantSpec WarrantSpec) ( return createdWarrant.ToWarrantSpec(), nil } -func (svc WarrantService) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]*WarrantSpec, error) { - warrantSpecs := make([]*WarrantSpec, 0) - warrants, err := svc.Repository.List(ctx, filterOptions, listParams) +func (svc WarrantService) List(ctx context.Context, filterParams *FilterParams, listParams service.ListParams) ([]WarrantSpec, error) { + warrantSpecs := make([]WarrantSpec, 0) + warrants, err := svc.Repository.List(ctx, filterParams, listParams) if err != nil { return warrantSpecs, err } for _, warrant := range warrants { - warrantSpecs = append(warrantSpecs, warrant.ToWarrantSpec()) + warrantSpecs = append(warrantSpecs, *warrant.ToWarrantSpec()) } return warrantSpecs, nil diff --git a/pkg/authz/warrant/warrant.go b/pkg/authz/warrant/spec.go similarity index 99% rename from pkg/authz/warrant/warrant.go rename to pkg/authz/warrant/spec.go index 175dface..f2dfdc42 100644 --- a/pkg/authz/warrant/warrant.go +++ b/pkg/authz/warrant/spec.go @@ -23,6 +23,8 @@ import ( "github.com/warrant-dev/warrant/pkg/service" ) +const Wildcard = "*" + type WarrantSpec struct { ObjectType string `json:"objectType" validate:"required,valid_object_type"` ObjectId string `json:"objectId" validate:"required,valid_object_id"` diff --git a/pkg/authz/warrant/warrant_test.go b/pkg/authz/warrant/spec_test.go similarity index 100% rename from pkg/authz/warrant/warrant_test.go rename to pkg/authz/warrant/spec_test.go diff --git a/pkg/authz/warrant/sqlite.go b/pkg/authz/warrant/sqlite.go index 5a3c50fe..b19197da 100644 --- a/pkg/authz/warrant/sqlite.go +++ b/pkg/authz/warrant/sqlite.go @@ -304,8 +304,7 @@ func (repo SQLiteRepository) GetByID(ctx context.Context, id int64) (Model, erro return &warrant, nil } -func (repo SQLiteRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { - offset := (listParams.Page - 1) * listParams.Limit +func (repo SQLiteRepository) List(ctx context.Context, filterParams *FilterParams, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) warrants := make([]Warrant, 0) query := ` @@ -315,53 +314,175 @@ func (repo SQLiteRepository) List(ctx context.Context, filterOptions *FilterOpti deletedAt IS NULL ` replacements := []interface{}{} + sortByColumn := listParams.SortBy - if filterOptions.ObjectType != "" { - query = fmt.Sprintf("%s AND objectType = ?", query) - replacements = append(replacements, filterOptions.ObjectType) + 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 filterOptions.ObjectId != "" { - query = fmt.Sprintf("%s AND objectId = ?", query) - replacements = append(replacements, filterOptions.ObjectId) + 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 filterOptions.Relation != "" { - query = fmt.Sprintf("%s AND relation = ?", query) - replacements = append(replacements, filterOptions.Relation) + 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 filterOptions.Subject != nil { - if filterOptions.Subject.ObjectType != "" { - query = fmt.Sprintf("%s AND subjectType = ?", query) - - replacements = append(replacements, filterOptions.Subject.ObjectType) + 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 filterOptions.Subject.ObjectId != "" { - query = fmt.Sprintf("%s AND subjectId = ?", query) + 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) + } - replacements = append(replacements, filterOptions.Subject.ObjectId) + 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.Policy != "" { + query = fmt.Sprintf("%s AND policyHash = ?", query) + replacements = append(replacements, filterParams.Policy.Hash()) + } - if filterOptions.Subject.Relation != "" { - query = fmt.Sprintf("%s AND subjectRelation = ?", query) + if listParams.AfterId != nil { + comparisonOp := "<" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = ">" + } - replacements = append(replacements, filterOptions.Subject.Relation) + switch listParams.AfterValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, listParams.DefaultSortBy(), comparisonOp) + replacements = append(replacements, listParams.AfterId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.AfterValue, + listParams.AfterId, + listParams.AfterValue, + ) + } } } - if filterOptions.Policy != "" { - query = fmt.Sprintf("%s AND policyHash = ?", query) - replacements = append(replacements, filterOptions.Policy.Hash()) + if listParams.BeforeId != nil { + comparisonOp := ">" + if listParams.SortOrder == service.SortOrderAsc { + comparisonOp = "<" + } + + switch listParams.BeforeValue { + case nil: + //nolint:gocritic + if listParams.SortBy == listParams.DefaultSortBy() { + query = fmt.Sprintf("%s AND %s %s ?", query, listParams.DefaultSortBy(), comparisonOp) + replacements = append(replacements, listParams.BeforeId) + } else if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? AND %s IS NULL)", query, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } else { + query = fmt.Sprintf("%s AND (%s IS NOT NULL OR (%s %s ? AND %s IS NULL))", query, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeId, + ) + } + default: + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s AND (%s %s ? OR %s IS NULL OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, sortByColumn, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } else { + query = fmt.Sprintf("%s AND (%s %s ? OR (%s %s ? AND %s = ?))", query, sortByColumn, comparisonOp, listParams.DefaultSortBy(), comparisonOp, sortByColumn) + replacements = append(replacements, + listParams.BeforeValue, + listParams.BeforeId, + listParams.BeforeValue, + ) + } + } } - if listParams.SortBy != "" { - query = fmt.Sprintf("%s ORDER BY %s %s", query, listParams.SortBy, listParams.SortOrder) + if listParams.BeforeId != nil { + if listParams.SortBy != listParams.DefaultSortBy() { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc, listParams.DefaultSortBy(), service.SortOrderDesc) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc, listParams.DefaultSortBy(), service.SortOrderAsc) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s, %s %s", query, sortByColumn, listParams.SortOrder, listParams.DefaultSortBy(), listParams.SortOrder) + } else { + if listParams.SortOrder == service.SortOrderAsc { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, sortByColumn, service.SortOrderDesc) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, sortByColumn, service.SortOrderAsc) + replacements = append(replacements, listParams.Limit) + } + query = fmt.Sprintf("With result_set AS (%s) SELECT * FROM result_set ORDER BY %s %s", query, sortByColumn, listParams.SortOrder) + } + } else { + if listParams.SortBy != listParams.DefaultSortBy() { + query = fmt.Sprintf("%s ORDER BY %s %s, %s %s LIMIT ?", query, sortByColumn, listParams.SortOrder, listParams.DefaultSortBy(), listParams.SortOrder) + replacements = append(replacements, listParams.Limit) + } else { + query = fmt.Sprintf("%s ORDER BY %s %s LIMIT ?", query, listParams.DefaultSortBy(), listParams.SortOrder) + replacements = append(replacements, listParams.Limit) + } } - query = fmt.Sprintf("%s LIMIT ?, ?", query) - replacements = append(replacements, offset, listParams.Limit) err := repo.DB.SelectContext( ctx, &warrants, diff --git a/pkg/event/spec.go b/pkg/event/spec.go index eb46f5ef..80a2f776 100644 --- a/pkg/event/spec.go +++ b/pkg/event/spec.go @@ -129,7 +129,7 @@ type LastIdSpec struct { func LastIdSpecToString(lastIdSpec LastIdSpec) (string, error) { jsonStr, err := json.Marshal(lastIdSpec) if err != nil { - return "", errors.Wrapf(err, "error mashaling lastId %v", lastIdSpec) + return "", errors.Wrapf(err, "error marshaling lastId %v", lastIdSpec) } return base64.StdEncoding.EncodeToString(jsonStr), nil diff --git a/pkg/authz/feature/handlers.go b/pkg/object/feature/handlers.go similarity index 99% rename from pkg/authz/feature/handlers.go rename to pkg/object/feature/handlers.go index 41f66946..c03ba457 100644 --- a/pkg/authz/feature/handlers.go +++ b/pkg/object/feature/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/feature/list.go b/pkg/object/feature/list.go similarity index 99% rename from pkg/authz/feature/list.go rename to pkg/object/feature/list.go index d9d48d42..d6306e74 100644 --- a/pkg/authz/feature/list.go +++ b/pkg/object/feature/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/feature/service.go b/pkg/object/feature/service.go similarity index 97% rename from pkg/authz/feature/service.go rename to pkg/object/feature/service.go index bc6c95ae..f04927b9 100644 --- a/pkg/authz/feature/service.go +++ b/pkg/object/feature/service.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/feature/spec.go b/pkg/object/feature/spec.go similarity index 94% rename from pkg/authz/feature/spec.go rename to pkg/object/feature/spec.go index 224aa4ed..be9e0f2d 100644 --- a/pkg/authz/feature/spec.go +++ b/pkg/object/feature/spec.go @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "time" "github.com/pkg/errors" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type FeatureSpec struct { - FeatureId string `json:"featureId" validate:"required,valid_object_id"` + FeatureId string `json:"featureId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/object/handlers.go b/pkg/object/handlers.go similarity index 99% rename from pkg/authz/object/handlers.go rename to pkg/object/handlers.go index e868d3f5..36a70bc0 100644 --- a/pkg/authz/object/handlers.go +++ b/pkg/object/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/object/list.go b/pkg/object/list.go similarity index 99% rename from pkg/authz/object/list.go rename to pkg/object/list.go index cd49646c..5521c916 100644 --- a/pkg/authz/object/list.go +++ b/pkg/object/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/object/model.go b/pkg/object/model.go similarity index 99% rename from pkg/authz/object/model.go rename to pkg/object/model.go index 38cde6e3..c1562c1f 100644 --- a/pkg/authz/object/model.go +++ b/pkg/object/model.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "encoding/json" diff --git a/pkg/authz/object/mysql.go b/pkg/object/mysql.go similarity index 92% rename from pkg/authz/object/mysql.go rename to pkg/object/mysql.go index dda7d88c..d1691fdf 100644 --- a/pkg/authz/object/mysql.go +++ b/pkg/object/mysql.go @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" "database/sql" "fmt" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -118,6 +119,47 @@ func (repo MySQLRepository) GetByObjectTypeAndId(ctx context.Context, objectType return &object, nil } +func (repo MySQLRepository) BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]Model, error) { + models := make([]Model, 0) + objects := make([]Object, 0) + if len(objectIds) == 0 { + return models, nil + } + + query, args, err := sqlx.In( + ` + SELECT id, objectType, objectId, meta, createdAt, updatedAt, deletedAt + FROM object + WHERE + objectType = ? AND + objectId IN (?) AND + deletedAt IS NULL + ORDER BY objectId ASC + `, + objectType, + objectIds, + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + err = repo.DB.SelectContext( + ctx, + &objects, + query, + args..., + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + for i := range objects { + models = append(models, &objects[i]) + } + + return models, nil +} + func (repo MySQLRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) objects := make([]Object, 0) diff --git a/pkg/authz/permission/handlers.go b/pkg/object/permission/handlers.go similarity index 99% rename from pkg/authz/permission/handlers.go rename to pkg/object/permission/handlers.go index c91a6d0d..1574c697 100644 --- a/pkg/authz/permission/handlers.go +++ b/pkg/object/permission/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/permission/list.go b/pkg/object/permission/list.go similarity index 99% rename from pkg/authz/permission/list.go rename to pkg/object/permission/list.go index 9993d657..c4e1fca8 100644 --- a/pkg/authz/permission/list.go +++ b/pkg/object/permission/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/permission/service.go b/pkg/object/permission/service.go similarity index 98% rename from pkg/authz/permission/service.go rename to pkg/object/permission/service.go index d466710c..ab60bf0a 100644 --- a/pkg/authz/permission/service.go +++ b/pkg/object/permission/service.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/permission/spec.go b/pkg/object/permission/spec.go similarity index 97% rename from pkg/authz/permission/spec.go rename to pkg/object/permission/spec.go index 6620af87..949ce7da 100644 --- a/pkg/authz/permission/spec.go +++ b/pkg/object/permission/spec.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "errors" "time" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type PermissionSpec struct { diff --git a/pkg/authz/object/postgres.go b/pkg/object/postgres.go similarity index 92% rename from pkg/authz/object/postgres.go rename to pkg/object/postgres.go index 62c39af3..45ff26f5 100644 --- a/pkg/authz/object/postgres.go +++ b/pkg/object/postgres.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" @@ -20,6 +20,7 @@ import ( "fmt" "regexp" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -119,6 +120,47 @@ func (repo PostgresRepository) GetByObjectTypeAndId(ctx context.Context, objectT return &object, nil } +func (repo PostgresRepository) BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]Model, error) { + models := make([]Model, 0) + objects := make([]Object, 0) + if len(objectIds) == 0 { + return models, nil + } + + query, args, err := sqlx.In( + ` + SELECT id, object_type, object_id, meta, created_at, updated_at, deleted_at + FROM object + WHERE + object_type = ? AND + object_id IN (?) AND + deleted_at IS NULL + ORDER BY object_id ASC + `, + objectType, + objectIds, + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + err = repo.DB.SelectContext( + ctx, + &objects, + query, + args..., + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + for i := range objects { + models = append(models, &objects[i]) + } + + return models, nil +} + func (repo PostgresRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) objects := make([]Object, 0) diff --git a/pkg/authz/pricingtier/handlers.go b/pkg/object/pricingtier/handlers.go similarity index 99% rename from pkg/authz/pricingtier/handlers.go rename to pkg/object/pricingtier/handlers.go index f6378642..e2e5720d 100644 --- a/pkg/authz/pricingtier/handlers.go +++ b/pkg/object/pricingtier/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/pricingtier/list.go b/pkg/object/pricingtier/list.go similarity index 99% rename from pkg/authz/pricingtier/list.go rename to pkg/object/pricingtier/list.go index 0d112c21..eb028c12 100644 --- a/pkg/authz/pricingtier/list.go +++ b/pkg/object/pricingtier/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/pricingtier/service.go b/pkg/object/pricingtier/service.go similarity index 98% rename from pkg/authz/pricingtier/service.go rename to pkg/object/pricingtier/service.go index b0a3666b..6f10cfd5 100644 --- a/pkg/authz/pricingtier/service.go +++ b/pkg/object/pricingtier/service.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/pricingtier/spec.go b/pkg/object/pricingtier/spec.go similarity index 97% rename from pkg/authz/pricingtier/spec.go rename to pkg/object/pricingtier/spec.go index aceb3026..55c0e7e6 100644 --- a/pkg/authz/pricingtier/spec.go +++ b/pkg/object/pricingtier/spec.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "errors" "time" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type PricingTierSpec struct { diff --git a/pkg/authz/object/repository.go b/pkg/object/repository.go similarity index 95% rename from pkg/authz/object/repository.go rename to pkg/object/repository.go index d6658af0..81c4a17f 100644 --- a/pkg/authz/object/repository.go +++ b/pkg/object/repository.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" @@ -27,6 +27,7 @@ type ObjectRepository interface { Create(ctx context.Context, object Model) (int64, error) GetById(ctx context.Context, id int64) (Model, error) GetByObjectTypeAndId(ctx context.Context, objectType string, objectId string) (Model, error) + BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]Model, error) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) UpdateByObjectTypeAndId(ctx context.Context, objectType string, objectId string, object Model) error DeleteByObjectTypeAndId(ctx context.Context, objectType string, objectId string) error diff --git a/pkg/authz/role/handlers.go b/pkg/object/role/handlers.go similarity index 99% rename from pkg/authz/role/handlers.go rename to pkg/object/role/handlers.go index f7f29b2d..c886a2b5 100644 --- a/pkg/authz/role/handlers.go +++ b/pkg/object/role/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/role/list.go b/pkg/object/role/list.go similarity index 98% rename from pkg/authz/role/list.go rename to pkg/object/role/list.go index cd920cb5..1c38295e 100644 --- a/pkg/authz/role/list.go +++ b/pkg/object/role/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/role/service.go b/pkg/object/role/service.go similarity index 97% rename from pkg/authz/role/service.go rename to pkg/object/role/service.go index 0f458460..9b2b6771 100644 --- a/pkg/authz/role/service.go +++ b/pkg/object/role/service.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/role/spec.go b/pkg/object/role/spec.go similarity index 94% rename from pkg/authz/role/spec.go rename to pkg/object/role/spec.go index 3fc29dc2..d7376e25 100644 --- a/pkg/authz/role/spec.go +++ b/pkg/object/role/spec.go @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "errors" "time" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type RoleSpec struct { - RoleId string `json:"roleId" validate:"required,valid_object_id"` + RoleId string `json:"roleId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/object/service.go b/pkg/object/service.go similarity index 90% rename from pkg/authz/object/service.go rename to pkg/object/service.go index 8e26030a..cc6e11fc 100644 --- a/pkg/authz/object/service.go +++ b/pkg/object/service.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" @@ -93,6 +93,25 @@ func (svc ObjectService) GetByObjectTypeAndId(ctx context.Context, objectType st return object.ToObjectSpec() } +func (svc ObjectService) BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]ObjectSpec, error) { + objects, err := svc.Repository.BatchGetByObjectTypeAndIds(ctx, objectType, objectIds) + if err != nil { + return nil, err + } + + objectSpecs := make([]ObjectSpec, 0) + for _, object := range objects { + objectSpec, err := object.ToObjectSpec() + if err != nil { + return nil, err + } + + objectSpecs = append(objectSpecs, *objectSpec) + } + + return objectSpecs, nil +} + func (svc ObjectService) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]ObjectSpec, error) { objectSpecs := make([]ObjectSpec, 0) objects, err := svc.Repository.List(ctx, filterOptions, listParams) diff --git a/pkg/authz/object/spec.go b/pkg/object/spec.go similarity index 99% rename from pkg/authz/object/spec.go rename to pkg/object/spec.go index 37a0ec39..aec14aed 100644 --- a/pkg/authz/object/spec.go +++ b/pkg/object/spec.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "encoding/json" diff --git a/pkg/authz/object/sqlite.go b/pkg/object/sqlite.go similarity index 92% rename from pkg/authz/object/sqlite.go rename to pkg/object/sqlite.go index de56d504..b5c84cf7 100644 --- a/pkg/authz/object/sqlite.go +++ b/pkg/object/sqlite.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" @@ -20,6 +20,7 @@ import ( "fmt" "time" + "github.com/jmoiron/sqlx" "github.com/pkg/errors" "github.com/warrant-dev/warrant/pkg/database" "github.com/warrant-dev/warrant/pkg/service" @@ -124,6 +125,47 @@ func (repo SQLiteRepository) GetByObjectTypeAndId(ctx context.Context, objectTyp return &object, nil } +func (repo SQLiteRepository) BatchGetByObjectTypeAndIds(ctx context.Context, objectType string, objectIds []string) ([]Model, error) { + models := make([]Model, 0) + objects := make([]Object, 0) + if len(objectIds) == 0 { + return models, nil + } + + query, args, err := sqlx.In( + ` + SELECT id, objectType, objectId, meta, createdAt, updatedAt, deletedAt + FROM object + WHERE + objectType = ? AND + objectId IN (?) AND + deletedAt IS NULL + ORDER BY objectId ASC + `, + objectType, + objectIds, + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + err = repo.DB.SelectContext( + ctx, + &objects, + query, + args..., + ) + if err != nil { + return models, errors.Wrap(err, "error getting objects batch") + } + + for i := range objects { + models = append(models, &objects[i]) + } + + return models, nil +} + func (repo SQLiteRepository) List(ctx context.Context, filterOptions *FilterOptions, listParams service.ListParams) ([]Model, error) { models := make([]Model, 0) objects := make([]Object, 0) diff --git a/pkg/authz/tenant/handlers.go b/pkg/object/tenant/handlers.go similarity index 100% rename from pkg/authz/tenant/handlers.go rename to pkg/object/tenant/handlers.go diff --git a/pkg/authz/tenant/list.go b/pkg/object/tenant/list.go similarity index 100% rename from pkg/authz/tenant/list.go rename to pkg/object/tenant/list.go diff --git a/pkg/authz/tenant/service.go b/pkg/object/tenant/service.go similarity index 98% rename from pkg/authz/tenant/service.go rename to pkg/object/tenant/service.go index b546c89b..231cbd47 100644 --- a/pkg/authz/tenant/service.go +++ b/pkg/object/tenant/service.go @@ -17,9 +17,9 @@ package tenant import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/tenant/spec.go b/pkg/object/tenant/spec.go similarity index 93% rename from pkg/authz/tenant/spec.go rename to pkg/object/tenant/spec.go index 38b4c7e5..10b405b7 100644 --- a/pkg/authz/tenant/spec.go +++ b/pkg/object/tenant/spec.go @@ -18,12 +18,12 @@ import ( "errors" "time" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type TenantSpec struct { - TenantId string `json:"tenantId" validate:"omitempty,valid_object_id"` + TenantId string `json:"tenantId" validate:"omitempty,valid_object_id"` Name *string `json:"name"` CreatedAt time.Time `json:"createdAt"` } diff --git a/pkg/authz/user/handlers.go b/pkg/object/user/handlers.go similarity index 99% rename from pkg/authz/user/handlers.go rename to pkg/object/user/handlers.go index 55ba582a..19fbda36 100644 --- a/pkg/authz/user/handlers.go +++ b/pkg/object/user/handlers.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "net/http" diff --git a/pkg/authz/user/list.go b/pkg/object/user/list.go similarity index 99% rename from pkg/authz/user/list.go rename to pkg/object/user/list.go index c5d4ea28..df3ccb05 100644 --- a/pkg/authz/user/list.go +++ b/pkg/object/user/list.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "fmt" diff --git a/pkg/authz/user/service.go b/pkg/object/user/service.go similarity index 97% rename from pkg/authz/user/service.go rename to pkg/object/user/service.go index 1147941f..f8db630a 100644 --- a/pkg/authz/user/service.go +++ b/pkg/object/user/service.go @@ -12,14 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "context" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" + object "github.com/warrant-dev/warrant/pkg/object" "github.com/warrant-dev/warrant/pkg/service" ) diff --git a/pkg/authz/user/spec.go b/pkg/object/user/spec.go similarity index 93% rename from pkg/authz/user/spec.go rename to pkg/object/user/spec.go index d93b9964..72017662 100644 --- a/pkg/authz/user/spec.go +++ b/pkg/object/user/spec.go @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -package authz +package object import ( "errors" "time" - object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" + object "github.com/warrant-dev/warrant/pkg/object" ) type UserSpec struct { - UserId string `json:"userId" validate:"omitempty,valid_object_id"` + UserId string `json:"userId" validate:"omitempty,valid_object_id"` Email *string `json:"email"` CreatedAt time.Time `json:"createdAt"` } diff --git a/pkg/service/json.go b/pkg/service/json.go index 298fc587..b1971521 100644 --- a/pkg/service/json.go +++ b/pkg/service/json.go @@ -28,10 +28,16 @@ import ( "github.com/rs/zerolog/log" ) +const ( + ObjectIdPattern = `^[a-zA-Z0-9_\-\.@\|:]+$` + ObjectTypePattern = `^[a-zA-Z0-9_\-]+$` + RelationPattern = `^[a-zA-Z0-9_\-]+$` +) + var validate *validator.Validate -var objectIdRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-\.@\|:]+$`) -var objectTypeRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`) -var relationRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`) +var objectIdRegexp = regexp.MustCompile(ObjectIdPattern) +var objectTypeRegexp = regexp.MustCompile(ObjectTypePattern) +var relationRegexp = regexp.MustCompile(RelationPattern) //nolint:errcheck func init() { diff --git a/pkg/service/middleware.go b/pkg/service/middleware.go index 15902fc5..efc4c958 100644 --- a/pkg/service/middleware.go +++ b/pkg/service/middleware.go @@ -167,7 +167,7 @@ func ListMiddleware[T ListParamParser](next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var err error listParamParser := ListParamParser(*new(T)) - listParams := defaultListParams(listParamParser) + listParams := DefaultListParams(listParamParser) urlQueryParams := r.URL.Query() if urlQueryParams.Has(paramNameQuery) { @@ -279,16 +279,16 @@ func GetListParamsFromContext[T ListParamParser](ctx context.Context) ListParams listParams, ok := ctxListParams.(*ListParams) if !ok { log.Ctx(ctx).Error().Msg("service: unsuccessful type cast of listParams context value to *ListParams type") - return defaultListParams(ListParamParser(*new(T))) + return DefaultListParams(ListParamParser(*new(T))) } return *listParams } - return defaultListParams(ListParamParser(*new(T))) + return DefaultListParams(ListParamParser(*new(T))) } -func defaultListParams(listParamParser ListParamParser) ListParams { +func DefaultListParams(listParamParser ListParamParser) ListParams { return ListParams{ Page: defaultPage, Limit: defaultLimit, diff --git a/tests/query.json b/tests/query.json new file mode 100644 index 00000000..68d55a86 --- /dev/null +++ b/tests/query.json @@ -0,0 +1,1789 @@ +{ + "ignoredFields": [ + "createdAt" + ], + "tests": [ + { + "name": "createDocumentObjectType", + "request": { + "method": "POST", + "url": "/v1/object-types", + "body": { + "type": "document", + "relations": { + "parent": { + "inheritIf": "parent", + "ofType": "document", + "withRelation": "parent" + }, + "owner": { + "inheritIf": "owner", + "ofType": "document", + "withRelation": "parent" + }, + "editor": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "editor", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "owner" + } + ] + }, + "viewer": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "viewer", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "editor" + } + ] + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + } + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "type": "document", + "relations": { + "parent": { + "inheritIf": "parent", + "ofType": "document", + "withRelation": "parent" + }, + "owner": { + "inheritIf": "owner", + "ofType": "document", + "withRelation": "parent" + }, + "editor": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "editor", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "owner" + } + ] + }, + "viewer": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "viewer", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "editor" + } + ] + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + } + } + } + } + }, + { + "name": "assignDocF1ParentDocD1", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D1", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + } + } + }, + { + "name": "assignDocF1ParentDocF2", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "F2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "F2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + } + } + }, + { + "name": "assignDocF2ParentDocD2", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + } + }, + { + "name": "assignDocF2ParentDocD3", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + } + }, + { + "name": "assignDocF2ParentDocF3", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "F3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "F3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + } + } + }, + { + "name": "assignDocF3ParentDocD4", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + } + } + }, + { + "name": "assignDocF3ParentDocD5", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + } + } + }, + { + "name": "assignUserU1OwnerDocF1", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + } + } + }, + { + "name": "assignUserU2EditorDocF2", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "F2", + "relation": "editor", + "subject": { + "objectType": "user", + "objectId": "U2" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "F2", + "relation": "editor", + "subject": { + "objectType": "user", + "objectId": "U2" + } + } + } + }, + { + "name": "assignUserU3ViewerDocF3", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + } + } + }, + { + "name": "assignUserU3OwnerDocD6", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "D6", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U3" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D6", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U3" + } + } + } + }, + { + "name": "updateDocumentD6", + "request": { + "method": "PUT", + "url": "/v1/objects/document/D6", + "body": { + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "D6", + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + } + } + }, + { + "name": "assignMemberOfRoleAdminOwnerOfAllDocs", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + } + } + }, + { + "name": "assignUserU4MemberOfRoleAdmin", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + } + } + }, + { + "name": "selectDocumentsWhereUserU3IsExplicitlyViewer", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20explicit%20document%20where%20user:U3%20is%20viewer" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + } + ] + } + } + }, + { + "name": "selectExplicitViewersOfTypeUserForDocumentF3", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20explicit%20viewer%20of%20type%20user%20for%20document:F3" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU3IsViewer", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U3%20is%20viewer" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D6", + "warrant": { + "objectType": "document", + "objectId": "D6", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": true, + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + }, + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU1IsEditorViewer", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U1%20is%20editor-viewer" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D1", + "warrant": { + "objectType": "document", + "objectId": "D1", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D2", + "warrant": { + "objectType": "document", + "objectId": "D2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D3", + "warrant": { + "objectType": "document", + "objectId": "D3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F1", + "warrant": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectExplicitOwnersOfTypeUserForDocumentD6", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20explicit%20owner%20of%20type%20user%20for%20document:D6" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U3", + "warrant": { + "objectType": "document", + "objectId": "D6", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectViewersOfTypeUserForDocumentD4", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20viewer%20of%20type%20user%20for%20document:D4" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U1", + "warrant": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + }, + "isImplicit": true + }, + { + "objectType": "user", + "objectId": "U2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "editor", + "subject": { + "objectType": "user", + "objectId": "U2" + } + }, + "isImplicit": true + }, + { + "objectType": "user", + "objectId": "U3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectEditorViewersOfTypeUserForDocumentD4", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20editor-viewer%20of%20type%20user%20for%20document:D4" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U1", + "warrant": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + }, + "isImplicit": true + }, + { + "objectType": "user", + "objectId": "U2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "editor", + "subject": { + "objectType": "user", + "objectId": "U2" + } + }, + "isImplicit": true + }, + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU4IsExplicitlyOwner", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20explicit%20document%20where%20user:U4%20is%20owner" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "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 + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU4IsViewer", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U4%20is%20viewer" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D1", + "warrant": { + "objectType": "document", + "objectId": "D1", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D2", + "warrant": { + "objectType": "document", + "objectId": "D2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D3", + "warrant": { + "objectType": "document", + "objectId": "D3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D6", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": true, + "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": true + }, + { + "objectType": "document", + "objectId": "F2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDesc", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U4%20is%20viewer&limit=3&sortBy=objectType&sortOrder=DESC" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "F3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "F1", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": true + } + ], + "lastId": "{{ selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDesc.lastId }}" + } + } + }, + { + "name": "selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDescAfterId", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U4%20is%20viewer&limit=3&sortBy=objectType&sortOrder=DESC&afterId={{ selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDesc.lastId }}" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D6", + "warrant": { + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subject": { + "objectType": "role", + "objectId": "admin", + "relation": "member" + } + }, + "isImplicit": true, + "meta": { + "name": "Document 6", + "title": "This is user:U3's document." + } + }, + { + "objectType": "document", + "objectId": "D5", + "warrant": { + "objectType": "document", + "objectId": "D5", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D4", + "warrant": { + "objectType": "document", + "objectId": "D4", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F3" + } + }, + "isImplicit": true + } + ], + "lastId": "{{ selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDescAfterId.lastId }}" + } + } + }, + { + "name": "selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDescAfterId2", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20document%20where%20user:U4%20is%20viewer&limit=3&sortBy=objectType&sortOrder=DESC&afterId={{ selectDocumentsWhereUserU4IsViewerLimit3SortByObjectTypeDescAfterId.lastId }}" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "document", + "objectId": "D3", + "warrant": { + "objectType": "document", + "objectId": "D3", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D2", + "warrant": { + "objectType": "document", + "objectId": "D2", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F2" + } + }, + "isImplicit": true + }, + { + "objectType": "document", + "objectId": "D1", + "warrant": { + "objectType": "document", + "objectId": "D1", + "relation": "parent", + "subject": { + "objectType": "document", + "objectId": "F1" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "selectMembersOrViewersOfTypeUserLimit3SortByObjectIdDesc", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortBy=objectId&sortOrder=DESC" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U4", + "warrant": { + "objectType": "role", + "objectId": "admin", + "relation": "member", + "subject": { + "objectType": "user", + "objectId": "U4" + } + }, + "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U3", + "warrant": { + "objectType": "document", + "objectId": "F3", + "relation": "viewer", + "subject": { + "objectType": "user", + "objectId": "U3" + } + }, + "isImplicit": false + }, + { + "objectType": "user", + "objectId": "U2", + "warrant": { + "objectType": "document", + "objectId": "F2", + "relation": "editor", + "subject": { + "objectType": "user", + "objectId": "U2" + } + }, + "isImplicit": true + } + ], + "lastId": "{{ selectMembersOrViewersOfTypeUserLimit3SortByObjectIdDesc.lastId }}" + } + } + }, + { + "name": "selectMembersOrViewersOfTypeUserLimit3SortByObjectIdDescAfterId", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20member%2Cviewer%20of%20type%20user&limit=3&sortBy=objectId&sortOrder=DESC&lastId={{ selectMembersOrViewersOfTypeUserLimit3SortByObjectIdDesc.lastId }}" + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "results": [ + { + "objectType": "user", + "objectId": "U1", + "warrant": { + "objectType": "document", + "objectId": "F1", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "U1" + } + }, + "isImplicit": true + } + ] + } + } + }, + { + "name": "updateDocumentObjectType", + "request": { + "method": "PUT", + "url": "/v1/object-types/document", + "body": { + "type": "document", + "relations": { + "parent": { + "inheritIf": "parent", + "ofType": "document", + "withRelation": "parent" + }, + "owner": { + "inheritIf": "owner", + "ofType": "document", + "withRelation": "parent" + }, + "editor": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "editor", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "owner" + } + ] + }, + "viewer": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "viewer", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "editor" + } + ] + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + }, + "nothing": { + "inheritIf": "noneOf", + "rules": [ + { + "inheritIf": "viewer" + } + ] + } + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "type": "document", + "relations": { + "parent": { + "inheritIf": "parent", + "ofType": "document", + "withRelation": "parent" + }, + "owner": { + "inheritIf": "owner", + "ofType": "document", + "withRelation": "parent" + }, + "editor": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "editor", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "owner" + } + ] + }, + "viewer": { + "inheritIf": "anyOf", + "rules": [ + { + "inheritIf": "viewer", + "ofType": "document", + "withRelation": "parent" + }, + { + "inheritIf": "editor" + } + ] + }, + "editor-viewer": { + "inheritIf": "allOf", + "rules": [ + { + "inheritIf": "editor" + }, + { + "inheritIf": "viewer" + } + ] + }, + "nothing": { + "inheritIf": "noneOf", + "rules": [ + { + "inheritIf": "viewer" + } + ] + } + } + } + } + }, + { + "name": "queryFailsWhenNoneOfIsUsed", + "request": { + "method": "GET", + "url": "/v1/query?q=select%20%2A" + }, + "expectedResponse": { + "statusCode": 400, + "body": { + "code": "invalid_request", + "message": "cannot query authorization models with object types that use the 'noneOf' operator." + } + } + }, + { + "name": "deleteDocumentF1", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/F1" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentF2", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/F2" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentF3", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/F3" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD1", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D1" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD2", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D2" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD3", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D3" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD4", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D4" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD5", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D5" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteDocumentD6", + "request": { + "method": "DELETE", + "url": "/v1/objects/document/D6" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserU1", + "request": { + "method": "DELETE", + "url": "/v1/objects/user/U1" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserU2", + "request": { + "method": "DELETE", + "url": "/v1/objects/user/U2" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserU3", + "request": { + "method": "DELETE", + "url": "/v1/objects/user/U3" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteUserU4", + "request": { + "method": "DELETE", + "url": "/v1/objects/user/U4" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteRoleAdmin", + "request": { + "method": "DELETE", + "url": "/v1/objects/role/admin" + }, + "expectedResponse": { + "statusCode": 200 + } + }, + { + "name": "deleteObjectTypeDocument", + "request": { + "method": "DELETE", + "url": "/v1/object-types/document" + }, + "expectedResponse": { + "statusCode": 200 + } + } + ] +} diff --git a/tests/warrant-list.json b/tests/warrants-list.json similarity index 74% rename from tests/warrant-list.json rename to tests/warrants-list.json index 14f62fd3..ce918475 100644 --- a/tests/warrant-list.json +++ b/tests/warrants-list.json @@ -3,6 +3,40 @@ "createdAt" ], "tests": [ + { + "name": "createdObjectTypeReport", + "request": { + "method": "POST", + "url": "/v1/object-types", + "body": { + "type": "report", + "relations": { + "owner": {}, + "editor": { + "inheritIf": "owner" + }, + "viewer": { + "inheritIf": "editor" + } + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "type": "report", + "relations": { + "owner": {}, + "editor": { + "inheritIf": "owner" + }, + "viewer": { + "inheritIf": "editor" + } + } + } + } + }, { "name": "assignPermissionViewBalanceSheetToRoleSeniorAccountant", "request": { @@ -89,6 +123,34 @@ } } }, + { + "name": "assignUserAOwnerOfReportA", + "request": { + "method": "POST", + "url": "/v1/warrants", + "body": { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } + } + }, + "expectedResponse": { + "statusCode": 200, + "body": { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } + } + } + }, { "name": "listLast2WarrantsSortByCreatedAtDesc", "request": { @@ -99,32 +161,32 @@ "statusCode": 200, "body": [ { - "objectType": "role", - "objectId": "senior-accountant", - "relation": "member", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", "subject": { "objectType": "user", "objectId": "user-a" - }, - "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" + } }, { - "objectType": "permission", - "objectId": "balance-sheet:edit", + "objectType": "role", + "objectId": "senior-accountant", "relation": "member", "subject": { "objectType": "user", - "objectId": "user-b" - } + "objectId": "user-a" + }, + "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" } ] } }, { - "name": "listWarrantsFilterByObjectTypePermission", + "name": "listWarrantsFilterByObjectTypePermissionOrReport", "request": { "method": "GET", - "url": "/v1/warrants?objectType=permission" + "url": "/v1/warrants?objectType=permission,report" }, "expectedResponse": { "statusCode": 200, @@ -146,6 +208,15 @@ "objectType": "user", "objectId": "user-b" } + }, + { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } } ] } @@ -179,7 +250,17 @@ }, "expectedResponse": { "statusCode": 200, - "body": [] + "body": [ + { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } + } + ] } }, { @@ -209,6 +290,15 @@ "objectId": "user-a" }, "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" + }, + { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } } ] } @@ -231,6 +321,15 @@ "objectId": "user-a" }, "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" + }, + { + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subject": { + "objectType": "user", + "objectId": "user-a" + } } ] } @@ -345,6 +444,16 @@ "statusCode": 200 } }, + { + "name": "deleteReportA", + "request": { + "method": "DELETE", + "url": "/v1/objects/report/report-a" + }, + "expectedResponse": { + "statusCode": 200 + } + }, { "name": "deleteUserA", "request": { @@ -364,6 +473,16 @@ "expectedResponse": { "statusCode": 200 } + }, + { + "name": "deleteObjectTypeReport", + "request": { + "method": "DELETE", + "url": "/v1/object-types/report" + }, + "expectedResponse": { + "statusCode": 200 + } } ] } diff --git a/tests/zz-events.json b/tests/zz-events.json index fad3ab4f..a224b1c5 100644 --- a/tests/zz-events.json +++ b/tests/zz-events.json @@ -17,23 +17,20 @@ { "type": "deleted", "source": "api", - "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceType": "object-type", + "resourceId": "report" }, { "type": "deleted", "source": "api", - "resourceType": "role", - "resourceId": "senior-accountant" + "resourceType": "user", + "resourceId": "user-b" }, { "type": "deleted", "source": "api", - "resourceType": "permission", - "resourceId": "view-balance-sheet" + "resourceType": "user", + "resourceId": "user-a" } ], "lastId": "{{ listLastThreeResourceEvents.lastId }}" @@ -53,11 +50,11 @@ { "type": "deleted", "source": "api", - "resourceType": "permission", - "resourceId": "balance-sheet:edit" + "resourceType": "report", + "resourceId": "report-a" }, { - "type": "created", + "type": "deleted", "source": "api", "resourceType": "permission", "resourceId": "balance-sheet:edit" @@ -93,35 +90,32 @@ { "type": "deleted", "source": "api", - "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceType": "object-type", + "resourceId": "report" }, { "type": "deleted", "source": "api", - "resourceType": "role", - "resourceId": "senior-accountant" + "resourceType": "user", + "resourceId": "user-b" }, { "type": "deleted", "source": "api", - "resourceType": "permission", - "resourceId": "view-balance-sheet" + "resourceType": "user", + "resourceId": "user-a" }, { "type": "deleted", "source": "api", - "resourceType": "permission", - "resourceId": "balance-sheet:edit" + "resourceType": "report", + "resourceId": "report-a" }, { "type": "deleted", "source": "api", - "resourceType": "user", - "resourceId": "user-b" + "resourceType": "permission", + "resourceId": "balance-sheet:edit" } ], "lastId": "{{ listResourceEventsFilterByType.lastId }}" @@ -142,37 +136,34 @@ "type": "deleted", "source": "api", "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceId": "user-b" }, { - "type": "created", + "type": "deleted", "source": "api", "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceId": "user-a" }, { - "type": "deleted", + "type": "created", "source": "api", "resourceType": "user", - "resourceId": "user-b" + "resourceId": "user-a" }, { - "type": "deleted", + "type": "created", "source": "api", "resourceType": "user", - "resourceId": "user-a" + "resourceId": "user-b" }, { - "type": "created", + "type": "deleted", "source": "api", "resourceType": "user", - "resourceId": "user-a" + "resourceId": "user-a", + "meta": { + "email": "user-a@warrant.dev" + } } ], "lastId": "{{ listResourceEventsFilterByResourceType.lastId }}" @@ -193,25 +184,22 @@ "type": "deleted", "source": "api", "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceId": "user-a" }, { "type": "created", "source": "api", "resourceType": "user", - "resourceId": "user-a", - "meta": { - "email": "user-a@warrant.dev" - } + "resourceId": "user-a" }, { "type": "deleted", "source": "api", "resourceType": "user", - "resourceId": "user-a" + "resourceId": "user-a", + "meta": { + "email": "user-a@warrant.dev" + } } ], "lastId": "{{ listResourceEventsFilterByResourceTypeAndResourceId.lastId }}" @@ -238,7 +226,16 @@ "subjectId": "senior-accountant" }, { - "type": "access_granted", + "type": "access_revoked", + "source": "api", + "objectType": "permission", + "objectId": "balance-sheet:edit", + "relation": "member", + "subjectType": "user", + "subjectId": "user-b" + }, + { + "type": "access_revoked", "source": "api", "objectType": "role", "objectId": "senior-accountant", @@ -248,15 +245,6 @@ "meta": { "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" } - }, - { - "type": "access_granted", - "source": "api", - "objectType": "permission", - "objectId": "balance-sheet:edit", - "relation": "member", - "subjectType": "user", - "subjectId": "user-a" } ], "lastId": "{{ listLastThreeAccessEvents.lastId }}" @@ -276,20 +264,23 @@ { "type": "access_granted", "source": "api", - "objectType": "permission", - "objectId": "view-balance-sheet", - "relation": "member", - "subjectType": "role", - "subjectId": "senior-accountant" + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subjectType": "user", + "subjectId": "user-a" }, { - "type": "access_revoked", + "type": "access_granted", "source": "api", - "objectType": "permission", - "objectId": "view-balance-sheet", + "objectType": "role", + "objectId": "senior-accountant", "relation": "member", - "subjectType": "role", - "subjectId": "senior-accountant" + "subjectType": "user", + "subjectId": "user-a", + "meta": { + "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" + } } ], "lastId": "{{ listNextTwoAccessEvents.lastId }}" @@ -319,6 +310,15 @@ "statusCode": 200, "body": { "events": [ + { + "type": "access_granted", + "source": "api", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subjectType": "user", + "subjectId": "user-a" + }, { "type": "access_granted", "source": "api", @@ -338,7 +338,7 @@ "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", - "subjectId": "user-a" + "subjectId": "user-b" }, { "type": "access_granted", @@ -348,18 +348,6 @@ "relation": "member", "subjectType": "role", "subjectId": "senior-accountant" - }, - { - "type": "access_granted", - "source": "api", - "objectType": "role", - "objectId": "senior-accountant", - "relation": "member", - "subjectType": "user", - "subjectId": "user-a", - "meta": { - "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" - } } ], "lastId": "{{ listAccessEventsFilterByType.lastId }}" @@ -376,6 +364,15 @@ "statusCode": 200, "body": { "events": [ + { + "type": "access_granted", + "source": "api", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subjectType": "user", + "subjectId": "user-a" + }, { "type": "access_revoked", "source": "api", @@ -412,15 +409,6 @@ "relation": "editor-viewer", "subjectType": "user", "subjectId": "user-a" - }, - { - "type": "access_allowed", - "source": "api", - "objectType": "report", - "objectId": "report-a", - "relation": "viewer", - "subjectType": "user", - "subjectId": "user-a" } ], "lastId": "{{ listAccessEventsFilterByObjectType.lastId }}" @@ -437,6 +425,15 @@ "statusCode": 200, "body": { "events": [ + { + "type": "access_granted", + "source": "api", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subjectType": "user", + "subjectId": "user-a" + }, { "type": "access_revoked", "source": "api", @@ -473,15 +470,6 @@ "relation": "editor-viewer", "subjectType": "user", "subjectId": "user-a" - }, - { - "type": "access_allowed", - "source": "api", - "objectType": "report", - "objectId": "report-a", - "relation": "viewer", - "subjectType": "user", - "subjectId": "user-a" } ], "lastId": "{{ listAccessEventsFilterByObjectTypeAndObjectId.lastId }}" @@ -498,6 +486,15 @@ "statusCode": 200, "body": { "events": [ + { + "type": "access_granted", + "source": "api", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", + "subjectType": "user", + "subjectId": "user-a" + }, { "type": "access_revoked", "source": "api", @@ -551,20 +548,6 @@ "tenant": "tenant-b" } } - }, - { - "type": "access_allowed", - "source": "api", - "objectType": "report", - "objectId": "report-a", - "relation": "owner", - "subjectType": "user", - "subjectId": "user-e", - "meta": { - "context": { - "tenant": "tenant-a" - } - } } ], "lastId": "{{ listAccessEventsFilterByObjectTypeAndObjectIdAndRelation.lastId }}" @@ -582,7 +565,16 @@ "body": { "events": [ { - "type": "access_granted", + "type": "access_revoked", + "source": "api", + "objectType": "permission", + "objectId": "balance-sheet:edit", + "relation": "member", + "subjectType": "user", + "subjectId": "user-b" + }, + { + "type": "access_revoked", "source": "api", "objectType": "role", "objectId": "senior-accountant", @@ -596,23 +588,14 @@ { "type": "access_granted", "source": "api", - "objectType": "permission", - "objectId": "balance-sheet:edit", - "relation": "member", + "objectType": "report", + "objectId": "report-a", + "relation": "owner", "subjectType": "user", "subjectId": "user-a" }, { - "type": "access_revoked", - "source": "api", - "objectType": "permission", - "objectId": "balance-sheet:edit", - "relation": "member", - "subjectType": "user", - "subjectId": "user-b" - }, - { - "type": "access_revoked", + "type": "access_granted", "source": "api", "objectType": "role", "objectId": "senior-accountant", @@ -626,14 +609,11 @@ { "type": "access_granted", "source": "api", - "objectType": "role", - "objectId": "senior-accountant", + "objectType": "permission", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", - "subjectId": "user-a", - "meta": { - "policy": "tenant == \"tenant-a\" \u0026\u0026 organization == \"org-a\"" - } + "subjectId": "user-b" } ], "lastId": "{{ listAccessEventsFilterBySubjectType.lastId }}" @@ -691,6 +671,16 @@ "statusCode": 200, "body": { "events": [ + { + "type": "access_granted", + "source": "api", + "objectType": "document", + "objectId": "*", + "relation": "owner", + "subjectType": "role", + "subjectId": "admin", + "subjectRelation": "member" + }, { "type": "access_revoked", "source": "api", @@ -723,19 +713,6 @@ "meta": { "policy": "tenant == \"tenant-a\"" } - }, - { - "type": "access_revoked", - "source": "api", - "objectType": "report", - "objectId": "*", - "relation": "owner", - "subjectType": "role", - "subjectId": "admin-b", - "subjectRelation": "member", - "meta": { - "policy": "tenant == \"tenant-b\"" - } } ], "lastId": "{{ listAccessEventsFilterBySubjectTypeAndSubjectRelation.lastId }}"