Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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