Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: compare this with ctx.subject (#1204) #1497

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
9 changes: 8 additions & 1 deletion contrib/namespace-type-lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ declare interface Boolean {}
declare interface String {}
declare interface Number {}
declare interface Function {}
declare interface Object {}
declare interface Object {
/**
* Placeholder to support `this.equals(ctx.subject)`
*
* @param element essentially `ctx.subject`
*/
equals(element: never): boolean
}
declare interface IArguments {}
declare interface RegExp {}

Expand Down
53 changes: 53 additions & 0 deletions internal/check/rewrites.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ func (e *Engine) checkSubjectSetRewrite(
Tuple: *tuple,
Type: ketoapi.TreeNodeNot,
}, e.checkInverted(ctx, tuple, c, restDepth)))
case *ast.SubjectEqualsObject:
checks = append(checks, checkgroup.WithEdge(checkgroup.Edge{
Tuple: *tuple,
Type: ketoapi.TreeNodeLeaf,
}, e.checkSubjectEqualsObject(ctx, tuple, restDepth)))

default:
return checkNotImplemented
Expand Down Expand Up @@ -175,6 +180,11 @@ func (e *Engine) checkInverted(
Tuple: *tuple,
Type: ketoapi.TreeNodeNot,
}, e.checkInverted(ctx, tuple, c, restDepth))
case *ast.SubjectEqualsObject:
check = checkgroup.WithEdge(checkgroup.Edge{
Tuple: *tuple,
Type: ketoapi.TreeNodeLeaf,
}, e.checkSubjectEqualsObject(ctx, tuple, restDepth))

default:
return checkNotImplemented
Expand All @@ -199,6 +209,49 @@ func (e *Engine) checkInverted(
}
}

// checkSubjectEqualsObject verifies that the subject and object are the same.
//
// Checks that the subject and object refer to the same entity. The check
// is performed by creating a subject from the object based on what the tuple subject type is.
// If the tuple subject is a SubjectSet, the subject's Namespace is used with the object. If the
// tuple subject is a SubjectID, the object's ID is used as a SubjectID.
// The object-subject and tuple subject are compared using Subject.Equals. This was added to support
// `this == ctx.subject` for identity permission cases. See https://github.com/ory/keto/issues/1204
func (e *Engine) checkSubjectEqualsObject(
_ context.Context,
r *relationTuple,
restDepth int,
) checkgroup.CheckFunc {
if restDepth < 0 {
e.d.Logger().Debug("reached max-depth, therefore this query will not be further expanded")
return checkgroup.UnknownMemberFunc
}

e.d.Logger().
WithField("request", r.String()).
Trace("check subject equals object")

var objAsSubj relationtuple.Subject
switch r.Subject.(type) {
case *relationtuple.SubjectSet:
objAsSubj = &relationtuple.SubjectSet{
Namespace: r.Namespace,
Object: r.Object,
}
case *relationtuple.SubjectID:
objAsSubj = &relationtuple.SubjectID{
ID: r.Object,
}
default:
return checkgroup.UnknownMemberFunc
}
if r.Subject.Equals(objAsSubj) {
return checkgroup.IsMemberFunc
}

return checkgroup.NotMemberFunc
}

// checkComputedSubjectSet rewrites the relation tuple to use the subject-set relation
// instead of the relation from the tuple.
//
Expand Down
32 changes: 25 additions & 7 deletions internal/check/rewrites_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,29 @@ var namespaces = []*namespace.Namespace{
{Name: "read",
SubjectSetRewrite: &ast.SubjectSetRewrite{
Children: ast.Children{
&ast.SubjectEqualsObject{},
&ast.ComputedSubjectSet{Relation: "viewer"},
&ast.ComputedSubjectSet{Relation: "owner"}}}},
{Name: "update",
SubjectSetRewrite: &ast.SubjectSetRewrite{
Children: ast.Children{
&ast.SubjectEqualsObject{},
&ast.ComputedSubjectSet{Relation: "owner"}}}},
{Name: "delete",
SubjectSetRewrite: &ast.SubjectSetRewrite{
Operation: ast.OperatorAnd,
Operation: ast.OperatorOr,
Children: ast.Children{
&ast.ComputedSubjectSet{Relation: "owner"},
&ast.TupleToSubjectSet{
Relation: "level",
ComputedSubjectSetRelation: "member"}}}},
&ast.SubjectSetRewrite{
Operation: ast.OperatorAnd,
Children: ast.Children{
&ast.ComputedSubjectSet{Relation: "owner"},
&ast.TupleToSubjectSet{
Relation: "level",
ComputedSubjectSetRelation: "member"},
},
},
&ast.SubjectEqualsObject{},
}}},
}},
{Name: "acl",
Relations: []ast.Relation{
Expand Down Expand Up @@ -192,9 +201,18 @@ func TestUsersetRewrites(t *testing.T) {
query: "resource:topsecret#delete@mark",
expected: checkgroup.ResultIsMember, // mark is both editor and has correct level
expectedPaths: []path{
{"*", "resource:topsecret#delete@mark", "level:superadmin#member@mark"},
{"*", "resource:topsecret#delete@mark", "resource:topsecret#owner@mark", "group:editors#member@mark"},
{"*", "*", "resource:topsecret#delete@mark", "level:superadmin#member@mark"},
{"*", "*", "resource:topsecret#delete@mark", "resource:topsecret#owner@mark", "group:editors#member@mark"},
},
}, {
query: "resource:topsecret#delete@topsecret",
expected: checkgroup.ResultIsMember, // topsecret may delete topsecret
}, {
query: "resource:topsecret#update@topsecret",
expected: checkgroup.ResultIsMember, // topsecret may update topsecret
}, {
query: "resource:topsecret#read@topsecret",
expected: checkgroup.ResultIsMember, // topsecret may read topsecret
}, {
query: "resource:topsecret#update@mike",
expected: checkgroup.ResultIsMember, // mike owns the resource
Expand Down
5 changes: 5 additions & 0 deletions internal/namespace/ast/ast_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type (
AsRewrite() *SubjectSetRewrite
}

SubjectEqualsObject struct{}

ComputedSubjectSet struct {
Relation string `json:"relation"`
}
Expand Down Expand Up @@ -69,3 +71,6 @@ func (t *TupleToSubjectSet) AsRewrite() *SubjectSetRewrite {
func (i *InvertResult) AsRewrite() *SubjectSetRewrite {
return &SubjectSetRewrite{Children: []Child{i}}
}
func (e *SubjectEqualsObject) AsRewrite() *SubjectSetRewrite {
return &SubjectSetRewrite{Children: []Child{e}}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"AccessToken": null,
"Account": [
{
"name": "token",
"types": [
{
"namespace": "AccessToken"
}
]
},
{
"name": "admin_token",
"types": [
{
"namespace": "AccessToken"
}
]
},
{
"name": "edit",
"rewrite": {
"operator": "or",
"children": [
{},
{
"relation": "admin_token"
}
]
}
},
{
"name": "view",
"rewrite": {
"operator": "or",
"children": [
{
"relation": "edit"
},
{
"relation": "token"
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"AccessToken": null,
"Account": [
{
"name": "token",
"types": [
{
"namespace": "AccessToken"
}
]
},
{
"name": "admin_token",
"types": [
{
"namespace": "AccessToken"
}
]
},
{
"name": "edit",
"rewrite": {
"operator": "or",
"children": [
{},
{
"relation": "admin_token"
}
]
}
},
{
"name": "view",
"rewrite": {
"operator": "or",
"children": [
{
"relation": "edit"
},
{
"relation": "token"
}
]
}
}
]
}
33 changes: 17 additions & 16 deletions internal/schema/itemtype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions internal/schema/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const (
itemOperatorOr // "||"
itemOperatorNot // "!"
itemOperatorAssign // "="
itemOperatorEquals // "=="
itemOperatorArrow // "=>"
itemOperatorDot // "."
itemOperatorColon // ":"
Expand Down Expand Up @@ -236,6 +237,7 @@ var oneRuneTokens = map[rune]itemType{
}

var multiRuneTokens = map[string]itemType{
"==": itemOperatorEquals,
"=>": itemOperatorArrow,
"||": itemOperatorOr,
"&&": itemOperatorAnd,
Expand Down
13 changes: 11 additions & 2 deletions internal/schema/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,14 +423,23 @@ func (p *parser) matchPropertyAccess(propertyName any) bool {
func (p *parser) parsePermissionExpression() (child ast.Child) {
var name, verb item

if !p.match("this", ".", &verb) {
switch {
case !p.match("this"):
return
case p.matchIf(is(itemOperatorEquals), "==", "ctx", ".", "subject"):
return &ast.SubjectEqualsObject{}
case !p.match(".", &verb):
return
}
if !p.matchPropertyAccess(&name) {
// failfast if verb.Val == "equals" so the matchPropertyAccess does not happen
if verb.Val != "equals" && !p.matchPropertyAccess(&name) {
return
}

switch verb.Val {
case "equals":
p.match("(", "ctx", ".", "subject", ")")
return &ast.SubjectEqualsObject{}
case "related":
if !p.match(".") {
return
Expand Down
42 changes: 42 additions & 0 deletions internal/schema/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,48 @@ class Resource implements Namespace {
"scope.action_1": (ctx: Context) => this.related["scope.relation"].traverse((r) => r.related["scope.relation"].includes(ctx.subject)),
"scope.action_2": (ctx: Context) => this.permits["scope.action_0"](ctx),
}
}`}, {"this == ctx.subject", `
import {Context, Namespace} from "@ory/keto-namespace-types"

class AccessToken implements Namespace {
}

class Account implements Namespace {
related: {
token: AccessToken[]
admin_token: AccessToken[]
}

permits = {
edit: (ctx: Context): boolean =>
this == ctx.subject ||
this.related.admin_token.includes(ctx.subject),

view: (ctx: Context): boolean =>
this.permits.edit(ctx) ||
this.related.token.includes(ctx.subject),
}
}`}, {"this.equals(ctx.subject)", `
import {Context, Namespace} from "@ory/keto-namespace-types"

class AccessToken implements Namespace {
}

class Account implements Namespace {
related: {
token: AccessToken[]
admin_token: AccessToken[]
}

permits = {
edit: (ctx: Context): boolean =>
this.equals(ctx.subject) ||
this.related.admin_token.includes(ctx.subject),

view: (ctx: Context): boolean =>
this.permits.edit(ctx) ||
this.related.token.includes(ctx.subject),
}
}`},
}

Expand Down
Loading