Skip to content

Commit

Permalink
Query API (#225)
Browse files Browse the repository at this point in the history
  • Loading branch information
kkajla12 authored Sep 26, 2023
1 parent 5b5ea26 commit 16479d5
Show file tree
Hide file tree
Showing 57 changed files with 3,810 additions and 349 deletions.
19 changes: 12 additions & 7 deletions cmd/warrant/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)

Expand All @@ -259,6 +263,7 @@ func main() {
objectTypeSvc,
permissionSvc,
pricingTierSvc,
querySvc,
roleSvc,
tenantSvc,
userSvc,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
56 changes: 56 additions & 0 deletions pkg/authz/query/handlers.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions pkg/authz/query/list.go
Original file line number Diff line number Diff line change
@@ -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)
}
176 changes: 176 additions & 0 deletions pkg/authz/query/parser.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 16479d5

Please sign in to comment.