I'm using Keto via the ory platform (not self host...
# talk-keto
c
I'm using Keto via the ory platform (not self hosted). Here is an attempted spec that doesn't even pass the syntax check but • I don't know why the syntax check fails • i don't understand how do deal with unique things
Copy code
import { Namespace, SubjectSet, Context } from "@ory/permission-namespace-types"

class LivingPerson implements Namespace {
}

class PUser implements Namespace {
  related: {
    livingPersons: LivingPerson[]
  }
}

class P implements Namespace {
  related: {
    members: PUser[]
    owners: PUser[]
    admins: PUser[]
  }
}

class UUR implements Namespace {
  related: {
    owningPs: P[]
  }
  permits = {
    view: (ctx: Context): boolean =>
      this.related.owningPs.traverse( (p): boolean =>
        p.related.admins.traverse((u): boolean =>
          u.related.livingPersons.traverse((l): boolean => (l == ctx.subject))))
  }
}
The syntax error as reported by the "permissions" tab, Permission Rules subtab, on the ory console is that on the line that has
Copy code
this.related.owningPs.traverse( (p): boolean =>
there is a syntax error at the "p". It says it expected => but got : The thing I wanted to express was that • UUR has exactly 1 owning P • PUser has exactly 1 LivingPerson I did both of this as arrays, because I didn't see any examples of doing them as singletons. Related: Can one express something like "traverse the relation R of BLAH only for BLAHs that have property X set?" If so, I guess I don't see how to do that when inputing the 'facts" about some particular BLAH. Any help would be grand.
w
I've implemented this on my fork. https://github.com/cmmoran/keto take a look at the branches. The https://github.com/cmmoran/keto/tree/feat/kitchen-sink branch has all of my updates as of now. ("this == ctx.subject", arbitrary depth traversal, arbitrary depth binary comparisons (the issue where you can only do binary ||, &&, etc on the top-level), etc.
arbitrary depth traverses are not support in "master" keto
c
ooh.
So, I should run my own keto on your branch until that gets rolled out to ory platform?
w
it would require (and did require) a fair refactor of the parser.
not sure if they'll accept my PR 🙂
c
Can it handle the idea of a singleton as I said in my orig post?
w
nah. zanzibar doesn't provide for that.
c
ok, that's fine. Is the syntax error spurious or related to the limited depth of traversals?
w
the
(p): boolean
is invalid.
remember. OPL is NOT typescript.
it's typescript-LIKE.
c
roger that. Thanks.
@witty-holiday-65473 do you have a script or something for running experiments on your branch from the commantd line?
it looks like their cat video is from a version before the ts-ish change
w
Hiya Ian, I do have some examples.
I have the examples broken down in 1. tuple sets to load into keto and 2. the rules definition .ts file. I wrote a simple script to make it easier for me to type as little as possible. 1131 tuples:
Copy code
add Project:administrators#access@alice
add Project:managers#access@bob
add Project:users#access@carol
add Role:admin#principal@Project:administrators
add Role:manager#principal@Project:managers
add Role:user#principal@Project:users
add Policy:createPolicy#attach@Role:admin
add Policy:editPolicy#attach@Role:manager
add Policy:readPolicy#attach@Role:user
add Resource:repository#create@Policy:createPolicy
add Resource:repository#edit@Policy:editPolicy
add Resource:repository#read@Policy:readPolicy
add ResourcePool:resourcePool#resources@Resource:repository
check Allowed ResourcePool:resourcePool#can_create@alice
check Allowed ResourcePool:resourcePool#can_edit@alice
check Allowed ResourcePool:resourcePool#can_read@alice
check Denied ResourcePool:resourcePool#can_create@bob
check Allowed ResourcePool:resourcePool#can_edit@bob
check Allowed ResourcePool:resourcePool#can_read@bob
check Denied ResourcePool:resourcePool#can_create@carol
check Denied ResourcePool:resourcePool#can_edit@carol
check Allowed ResourcePool:resourcePool#can_read@carol
check Denied ResourcePool:resourcePool#can_create@repository
check Denied ResourcePool:resourcePool#can_edit@repository
check Denied ResourcePool:resourcePool#can_read@repository
check Allowed ResourcePool:resourcePool#can_edit@resourcePool
definition file:
Copy code
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { Context, Namespace } from "@ory/keto-namespace-types"

class User implements Namespace {}

class Project implements Namespace {
  related: {
    access: User[]
  }
}

class Role implements Namespace {
  related: {
    principal: Project[]
  }

  permits = {
    can_assume: (ctx: Context): boolean =>
      this.related.principal.traverse((p) =>
        p.related.access.includes(ctx.subject),
      ),
  }
}

class Policy implements Namespace {
  related: {
    attach: Role[]
  }
}

class Resource implements Namespace {
  related: {
    create: Policy[]
    edit: Policy[]
    read: Policy[]
  }

  permits = {
    can_create: (ctx: Context): boolean =>
      this.related.create.traverse((p) =>
        p.related.attach.traverse((r) => r.permits.can_assume(ctx)),
      ),
    can_edit: (ctx: Context): boolean =>
      this.related.edit.traverse((p) =>
        p.related.attach.traverse((r) => r.permits.can_assume(ctx)),
      ),
    can_read: (ctx: Context): boolean =>
      this.related.read.traverse((p) =>
        p.related.attach.traverse((r) => r.permits.can_assume(ctx)),
      ),
  }
}

class ResourcePool implements Namespace {
  related: {
    resources: Resource[]
  }

  permits = {
    can_create: (ctx: Context): boolean =>
      this.related.resources.traverse((r) => r.permits.can_create(ctx)),
    can_edit: (ctx: Context): boolean =>
      this.equals(ctx.subject) ||
      this.permits.can_create(ctx) ||
      this.related.resources.traverse((r) => r.permits.can_edit(ctx)),
    can_read: (ctx: Context): boolean =>
      this.permits.can_edit(ctx) ||
      this.related.resources.traverse((r) => r.permits.can_read(ctx)),
  }
}
keep in mind this will not work on master. only on my fork(s).
kitchen-sink
is the one that has all the things. give that one a go and let me know if you have questions. the "cmd" script i use to "add" "check" and "del" tuples from the first "tuple file" is here:
Copy code
#!/usr/bin/env bash

ansi() {
  if ! [[ $2 =~ ^[0-9]+ ]] || [[ -z "$3" ]]; then
    echo -e "\e[${1}m${*:2}\e[0m"
  else
    echo -e "\e[${1}m$(printf "%*s" ${2} ${*:3})\e[0m"
  fi
}
red() {
  ansi 31 "$@"
}
green() {
  ansi 32 "$@"
}
yellow() {
  ansi 33 "$@"
}
blue() {
  ansi 34 "$@"
}
gray() {
  ansi 90 "$@"
}


file="$1"
cmd="$2"

if [[ "$file" == "del-all" ]]; then
  keto relation-tuple delete-all --force $is
  exit 0
fi
if [[ "$file" == "parse" ]]; then
  echo 'flowchart LR
    %%{init: {"flowchart": {"defaultRenderer": "elk"}} }%%'
  keto relation-tuple get --format json-pretty $is | jq -r '.|.relation_tuples|.[]|
  if(.subject_set != null) then
    "    ".subject_set.object+" --> |"+.relation+"| "+.object
  else
    "    "+.subject_id+"(("+.subject_id+"))"+" --> |"+.relation+"| "+.object
  end' | sort
  exit 0
fi

if [[ -z "$file" ]]; then
  printf "$0 <file> <cmd>\n\tfile: the file to process\n\tcmd: the cmd to filter\n\nAt least <file> is required\n\n"
  exit 1
fi

if [[ "$cmd" == "parse" ]]; then
  data=$(cat $file | jq -r '.[]|.relation_tuple|if(.subject_set != null) then "add "+.namespace+":"+.object+"#"+.relation+"@"+.subject_set.namespace+":"+.subject_set.object else "add "+.namespace+":"+.object+"#"+.relation+"@"+.subject_id end')
  if [[ -z "$data" ]]; then
    data=$(cat $file | jq -r '.relation_tuples|.[]|if(.subject_set != null) then "add "+.namespace+":"+.object+"#"+.relation+"@"+.subject_set.namespace+":"+.subject_set.object else "add "+.namespace+":"+.object+"#"+.relation+"@"+.subject_id end' 2>/dev/null)
  fi
  echo "$data" > $file
  exit 0
fi
readarray -t commands < "$file"

if [[ -z "$cmd" ]]; then
  hascmd=""
else
  hascmd="1"
fi

for line in "${commands[@]}"
do
  if [[ "$line" =~ ^[[:space:]]*#.*$ ]]; then
    gray "$line"
    continue
  fi
  read -a params <<< "$line"

  if [[ -z "$hascmd" ]]; then
    cmd="${params[0]}"
  fi

  if [[ "$cmd" =~ add|del|check|checkAll ]]; then
    if [[ "${params[0]}" == "add" ]]; then
      if [[ "$cmd" == "del" ]]; then
        echo "Removing: $(echo "${params[1]}")"
        echo "${params[1]}" | keto relation-tuple parse - --format json | keto relation-tuple delete - >/dev/null $is
      elif [[ "$cmd" == "add" ]]; then
        exists="$(echo "${params[1]}" | keto relation-tuple parse - --format json | $(jq -r 'if(.subject_set != null) then "keto relation-tuple get -q --namespace "+.namespace+" --relation "+.relation+" --object "+.object+" --subject-set "+.subject_set.namespace+":"+.subject_set.object+"#"+.subject_set.relation+" '$is'" else "keto relation-tuple get -q --namespace "+.namespace+" --relation "+.relation+" --object "+" "+.object+" --subject-id "+.subject_id+" '$is'" end'))"
        if [[ -z "$exists" ]]; then
          echo "Inserting: ${params[1]}"
          echo "${params[1]}" | keto relation-tuple parse - --format json | keto relation-tuple create - >/dev/null $is
        else
          gray "Relation exists: $exists"
        fi
      fi
    elif [[ "${params[0]}" == "check" && "$cmd" == "check" ]]; then
      expect="${params[1]}"
      tuple="${params[2]}"
      result="$(keto check `echo "$tuple" | keto relation-tuple parse - --format json | jq -r 'if(.subject_set != null) then .subject_set.namespace+":"+.subject_set.object+"#"+.subject_set.relation+" "+.relation+" "+.namespace+" "+.object else .subject_id+" "+.relation+" "+.namespace+" "+.object end'` $is 2>/dev/null)"
      if [[ "$expect" == "$result" ]]; then
        printf "%s %s %s\n" "Check:" "$(green 7 "$expect")" "$tuple"
      else
        printf "%s %s %s\n" "Check:" "$(red 7 "$expect")" "$tuple"
      fi
    elif [[ "${params[0]}" == "checkAll" && "$cmd" == "checkAll" ]]; then
        IFS=', ' read -r -a obj_list <<< "${params[1]}"
        IFS=', ' read -r -a rel_list <<< "${params[2]}"
        IFS=', ' read -r -a sub_list <<< "${params[3]}"
        let count=$((${#obj_list[@]}*${#rel_list[@]}*${#sub_list[@]}))
        let i=0
        for sub in "${sub_list[@]}"
        do
          for rel in "${rel_list[@]}"
          do
            for obj in "${obj_list[@]}"
            do
              i=$((i+1))
              tuple="$obj#$rel@$sub"
              ptuple="$(ansi 96 "$obj")#$(ansi 95 "$rel")@$(ansi 92 "$sub")"
              result="$(keto check `echo "$tuple" | keto relation-tuple parse - --format json | jq -r 'if(.subject_set != null) then .subject_set.namespace+":"+.subject_set.object+"#"+.subject_set.relation+" "+.relation+" "+.namespace+" "+.object else .subject_id+" "+.relation+" "+.namespace+" "+.object end'` $is 2>/dev/null)"
              result="$(yellow 7 $result)"
              printf "[%3d of %3d] %s %s %s\n" $i $count "Check:" "$result" "$ptuple"
            done
          done
        done
    fi
  fi
done
don't judge my bash script. i realize i could have just used getopt(s) and it would have been cleaner but a "perfect test script" was not my goal.
^ one feature that script has is that it will read the entire relation model from all relationships within ketos db and print you a pretty mermaid-js model that you can take to mermaid js's live site and paste it and see neat things.
cmd parse
c
I'm sorry I missed this when you posted it. I will study in the next day or so. Thanks VERY much.