{
  "id": "P109",
  "slug": "permissions",
  "key": "P109-permissions",
  "title": "Permissions",
  "summary": "Roles as sets of actions in a partial order; folders that grant roles and deny actions, inherited by every document below them with the reason for each action; and documents moved without their stored grants being refreshed, audited for grants too many and too few.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P109 - Permissions\n\nRoles as sets of actions, folders that grant roles to users and can deny\nactions, and documents that inherit from every folder above them. The\nprogram also shows what happens when a document is moved and its stored\ngrants are not refreshed.\n\n- `main.eml` - the menu, the roles and how they compare, the folder tree,\n  one user on one document, moves, writes and the audit\n- `perms.eml` - the actions and roles, subset questions on lists, the\n  order between roles, the folder chain, effective permissions, refresh\n  and audit\n\nHow each part works:\n\n- Roles: viewer (read), editor (read, write), auditor (read, export) and\n  owner (all five actions). As in the corpus case\n  `permission-set-algebra`, one role is below another when every one of\n  its actions is in the other. That is a partial order: editor and auditor\n  each have an action the other lacks, so neither is below the other.\n- The program lists the roles directly above one another, and sorts the\n  roles by \"below\" from two starting orders. Editor and auditor keep\n  whichever order they came in, so the sort has no single answer.\n- Effective permissions: the union of every role granted to the user on\n  the document's folder and on every folder above it, minus every action\n  denied there. A deny wins over any grant. Each allowed action names the\n  nearest folder that gives it.\n- Stored grants, as in the corpus case `inherited-permission-after-move`:\n  each document keeps what its folder gave each user when it was last\n  written. A move changes the folder, not the document, so nothing is\n  refreshed. The audit lists every grant where the stored answer and the\n  folder's answer differ, too much or too little. Writing a document\n  refreshes its grants.\n\nWhat is checked: menu choices 1 to 7; one of the five users; one of the\ndocuments; one of the four folders; a move to a different folder. An\nempty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- the roles, their order, and the two sorts;\n- the folder tree;\n- ben on contract.pdf (denied in legal), cy (auditor), dee on plan.txt,\n  and guest on plan.txt (nothing);\n- the audit with nothing out of date;\n- contract.pdf moved to public and brochure.pdf to legal, the tree again,\n  guest on both, and the audit: 3 grants too many and 3 too few;\n- contract.pdf written, and the audit with its entries gone.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a cancel, an unknown user, a cancel and an unknown document;\n- moves: a cancel, an unknown document, a cancel, an unknown folder, and\n  a move to the folder it is in;\n- writes: a cancel and an unknown document.\n\nBuilt on the verified corpus cases `permission-set-algebra` (for sets,\n\"less than\" means subset, and the order is partial) and\n`inherited-permission-after-move` (the grants stored on a document are\nrefreshed on a write, and a move is not a write).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P109 permissions: roles compared as sets, folders that grant and deny,\n# what a user may do with a document and why, and documents moved without\n# their stored grants being refreshed.\nimport perms\n\ndef plain(s):\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s + \" \" => s\n    return s\n\ndef listed(xs):\n    if len(xs) == 0:\n        return \"nothing\"\n    \"\" => out\n    for x in xs:\n        if out != \"\":\n            out + \", \" => out\n        out + x => out\n    return out\n\ndef show_roles():\n    perms.roles() => r\n    for n in perms.role_names():\n        (\"  \" + pad(n, 9) + listed(r[n])) ^0\n    perms.role_names() => names\n    for i in [0:len(names) - 1]:\n        for j in [i + 1:len(names) - 1]:\n            perms.compare(names[i], names[j]) => c\n            if c == \"below\":\n                (\"  \" + names[i] + \" < \" + names[j] + \": every action of \" + names[i] + \" is in \" + names[j]) ^0\n            elif c == \"above\":\n                (\"  \" + names[i] + \" > \" + names[j]) ^0\n            elif c == \"neither\":\n                (\"  \" + names[i] + \" and \" + names[j] + \": neither - each has an action the other lacks, so <, <=, > and >= are all false\") ^0\n    \"\" => line\n    for c in perms.covers():\n        if line != \"\":\n            line + \", \" => line\n        line + c[0] + \" -> \" + c[1] => line\n    (\"Directly above one another: \" + line + \".\") ^0\n    perms.sort_by_less([\"editor\", \"auditor\", \"viewer\", \"owner\"]) => a\n    perms.sort_by_less([\"auditor\", \"editor\", \"viewer\", \"owner\"]) => b\n    (\"Sorted by <, from two starting orders: \" + listed(a) + \" / \" + listed(b) + \". Roles that are neither keep whatever order they came in.\") ^0\n\ndef show_tree(folders, docs):\n    for f in [\"company\", \"team\", \"legal\", \"public\"]:\n        folders[f] => fo\n        \"\" => g\n        for u in [\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"]:\n            if u in fo[1]:\n                if g != \"\":\n                    g + \", \" => g\n                g + u + \" \" + fo[1][u] => g\n        for u in [\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"]:\n            if u in fo[2]:\n                if g != \"\":\n                    g + \"; \" => g\n                g + \"deny \" + u + \" \" + listed(fo[2][u]) => g\n        \"\" => indent\n        if fo[0] != \"\":\n            \"  \" => indent\n        (\"  \" + pad(indent + f, 11) + g) ^0\n        \"\" => here\n        for d in perms.sorted_docs(docs):\n            if docs[d][0] == f:\n                if here != \"\":\n                    here + \", \" => here\n                here + d => here\n        if here != \"\":\n            (\"  \" + indent + \"  documents: \" + here) ^0\n\ndef check(folders, docs):\n    plain(input(\"user (ana, ben, cy, dee or guest)> \")) => u\n    if u == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not perms.has([\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"], u):\n        \"Type ana, ben, cy, dee or guest.\" ^0\n        return\n    plain(input(\"document (\" + listed(perms.sorted_docs(docs)) + \")> \")) => d\n    if d == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not (d in docs):\n        \"Pick one of the documents listed.\" ^0\n        return\n    perms.effective(folders, docs[d][0], u) => e\n    (u + \" on \" + d + \" (in \" + docs[d][0] + \"): \" + listed(e[0]) + \".\") ^0\n    for r in e[1]:\n        (\"  \" + pad(r[0], 7) + r[1]) ^0\n    docs[d][1][u] => cached\n    if not perms.subset(cached, e[0]) or not perms.subset(e[0], cached):\n        (\"  The grants stored on the document still say: \" + listed(perms.ordered(cached)) + \".\") ^0\n\ndef move(folders, docs):\n    plain(input(\"document> \")) => d\n    if d == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not (d in docs):\n        \"Pick one of the documents listed.\" ^0\n        return\n    plain(input(\"to folder (company, team, legal or public)> \")) => f\n    if f == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not (f in folders):\n        \"Type company, team, legal or public.\" ^0\n        return\n    if docs[d][0] == f:\n        (d + \" is already in \" + f + \".\") ^0\n        return\n    docs[d][0] => old\n    f => docs[d][0]\n    (d + \" moved from \" + old + \" to \" + f + \". Its stored grants were not refreshed: a move changes the folder, not the document.\") ^0\n\ndef edit(folders, docs, users):\n    plain(input(\"document> \")) => d\n    if d == \"\":\n        \"Cancelled.\" ^0\n        return\n    if not (d in docs):\n        \"Pick one of the documents listed.\" ^0\n        return\n    perms.refresh(folders, docs, d, users)\n    (d + \" written: its stored grants were refreshed from \" + docs[d][0] + \".\") ^0\n\ndef show_audit(folders, docs, users):\n    perms.audit(folders, docs, users) => a\n    if len(a) == 0:\n        \"Every stored grant matches what the folders give now.\" ^0\n        return\n    0 => over\n    0 => under\n    for x in a:\n        if x[3] == \"over\":\n            over + 1 => over\n            (\"  \" + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + \"stored yes, folder no - more than it should\") ^0\n        else:\n            under + 1 => under\n            (\"  \" + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + \"stored no, folder yes - less than it should\") ^0\n    (str(len(a)) + \" stored grants are out of date: \" + str(over) + \" give more than the folder does, \" + str(under) + \" less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.\") ^0\n\n{\"company\": [\"\", {\"ana\": \"owner\", \"ben\": \"viewer\", \"cy\": \"viewer\"}, {}], \"team\": [\"company\", {\"ben\": \"editor\", \"dee\": \"editor\"}, {}], \"legal\": [\"company\", {\"cy\": \"auditor\"}, {\"ben\": [\"read\"]}], \"public\": [\"company\", {\"guest\": \"viewer\"}, {}]} => folders\n{\"plan.txt\": [\"team\", {}], \"notes.txt\": [\"team\", {}], \"contract.pdf\": [\"legal\", {}], \"brochure.pdf\": [\"public\", {}]} => docs\n[\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"] => users\nfor d in perms.sorted_docs(docs):\n    perms.refresh(folders, docs, d, users)\n\n\"== Permissions ==\" ^0\n\"Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        show_roles()\n    elif choice == \"2\":\n        show_tree(folders, docs)\n    elif choice == \"3\":\n        check(folders, docs)\n    elif choice == \"4\":\n        move(folders, docs)\n    elif choice == \"5\":\n        edit(folders, docs, users)\n    elif choice == \"6\":\n        show_audit(folders, docs, users)\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import perms\n\ndef plain(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef pad(s, n):\n    while len(s) < n:\n        s = s + \" \"\n    return s\n\ndef listed(xs):\n    if len(xs) == 0:\n        return \"nothing\"\n    out = \"\"\n    for x in xs:\n        if out != \"\":\n            out = out + \", \"\n        out = out + x\n    return out\n\ndef show_roles():\n    r = perms.roles()\n    for n in perms.role_names():\n        print(\"  \" + pad(n, 9) + listed(r[n]))\n    names = perms.role_names()\n    for i in range(0, len(names)):\n        for j in range(i + 1, len(names)):\n            c = perms.compare(names[i], names[j])\n            if c == \"below\":\n                print(\"  \" + names[i] + \" < \" + names[j] + \": every action of \" + names[i] + \" is in \" + names[j])\n            elif c == \"above\":\n                print(\"  \" + names[i] + \" > \" + names[j])\n            elif c == \"neither\":\n                print(\"  \" + names[i] + \" and \" + names[j] + \": neither - each has an action the other lacks, so <, <=, > and >= are all false\")\n    line = \"\"\n    for c in perms.covers():\n        if line != \"\":\n            line = line + \", \"\n        line = line + c[0] + \" -> \" + c[1]\n    print(\"Directly above one another: \" + line + \".\")\n    a = perms.sort_by_less([\"editor\", \"auditor\", \"viewer\", \"owner\"])\n    b = perms.sort_by_less([\"auditor\", \"editor\", \"viewer\", \"owner\"])\n    print(\"Sorted by <, from two starting orders: \" + listed(a) + \" / \" + listed(b) + \". Roles that are neither keep whatever order they came in.\")\n\ndef show_tree(folders, docs):\n    for f in [\"company\", \"team\", \"legal\", \"public\"]:\n        fo = folders[f]\n        g = \"\"\n        for u in [\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"]:\n            if u in fo[1]:\n                if g != \"\":\n                    g = g + \", \"\n                g = g + u + \" \" + fo[1][u]\n        for u in [\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"]:\n            if u in fo[2]:\n                if g != \"\":\n                    g = g + \"; \"\n                g = g + \"deny \" + u + \" \" + listed(fo[2][u])\n        indent = \"\"\n        if fo[0] != \"\":\n            indent = \"  \"\n        print(\"  \" + pad(indent + f, 11) + g)\n        here = \"\"\n        for d in perms.sorted_docs(docs):\n            if docs[d][0] == f:\n                if here != \"\":\n                    here = here + \", \"\n                here = here + d\n        if here != \"\":\n            print(\"  \" + indent + \"  documents: \" + here)\n\ndef check(folders, docs):\n    u = plain(input(\"user (ana, ben, cy, dee or guest)> \"))\n    if u == \"\":\n        print(\"Cancelled.\")\n        return\n    if not perms.has([\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"], u):\n        print(\"Type ana, ben, cy, dee or guest.\")\n        return\n    d = plain(input(\"document (\" + listed(perms.sorted_docs(docs)) + \")> \"))\n    if d == \"\":\n        print(\"Cancelled.\")\n        return\n    if not d in docs:\n        print(\"Pick one of the documents listed.\")\n        return\n    e = perms.effective(folders, docs[d][0], u)\n    print(u + \" on \" + d + \" (in \" + docs[d][0] + \"): \" + listed(e[0]) + \".\")\n    for r in e[1]:\n        print(\"  \" + pad(r[0], 7) + r[1])\n    cached = docs[d][1][u]\n    if not perms.subset(cached, e[0]) or not perms.subset(e[0], cached):\n        print(\"  The grants stored on the document still say: \" + listed(perms.ordered(cached)) + \".\")\n\ndef move(folders, docs):\n    d = plain(input(\"document> \"))\n    if d == \"\":\n        print(\"Cancelled.\")\n        return\n    if not d in docs:\n        print(\"Pick one of the documents listed.\")\n        return\n    f = plain(input(\"to folder (company, team, legal or public)> \"))\n    if f == \"\":\n        print(\"Cancelled.\")\n        return\n    if not f in folders:\n        print(\"Type company, team, legal or public.\")\n        return\n    if docs[d][0] == f:\n        print(d + \" is already in \" + f + \".\")\n        return\n    old = docs[d][0]\n    docs[d][0] = f\n    print(d + \" moved from \" + old + \" to \" + f + \". Its stored grants were not refreshed: a move changes the folder, not the document.\")\n\ndef edit(folders, docs, users):\n    d = plain(input(\"document> \"))\n    if d == \"\":\n        print(\"Cancelled.\")\n        return\n    if not d in docs:\n        print(\"Pick one of the documents listed.\")\n        return\n    perms.refresh(folders, docs, d, users)\n    print(d + \" written: its stored grants were refreshed from \" + docs[d][0] + \".\")\n\ndef show_audit(folders, docs, users):\n    a = perms.audit(folders, docs, users)\n    if len(a) == 0:\n        print(\"Every stored grant matches what the folders give now.\")\n        return\n    over = 0\n    under = 0\n    for x in a:\n        if x[3] == \"over\":\n            over = over + 1\n            print(\"  \" + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + \"stored yes, folder no - more than it should\")\n        else:\n            under = under + 1\n            print(\"  \" + pad(x[0], 13) + pad(x[1], 6) + pad(x[2], 7) + \"stored no, folder yes - less than it should\")\n    print(str(len(a)) + \" stored grants are out of date: \" + str(over) + \" give more than the folder does, \" + str(under) + \" less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.\")\n\nfolders = {\"company\": [\"\", {\"ana\": \"owner\", \"ben\": \"viewer\", \"cy\": \"viewer\"}, {}], \"team\": [\"company\", {\"ben\": \"editor\", \"dee\": \"editor\"}, {}], \"legal\": [\"company\", {\"cy\": \"auditor\"}, {\"ben\": [\"read\"]}], \"public\": [\"company\", {\"guest\": \"viewer\"}, {}]}\ndocs = {\"plan.txt\": [\"team\", {}], \"notes.txt\": [\"team\", {}], \"contract.pdf\": [\"legal\", {}], \"brochure.pdf\": [\"public\", {}]}\nusers = [\"ana\", \"ben\", \"cy\", \"dee\", \"guest\"]\nfor d in perms.sorted_docs(docs):\n    perms.refresh(folders, docs, d, users)\nprint(\"== Permissions ==\")\nprint(\"Roles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        show_roles()\n    elif choice == \"2\":\n        show_tree(folders, docs)\n    elif choice == \"3\":\n        check(folders, docs)\n    elif choice == \"4\":\n        move(folders, docs)\n    elif choice == \"5\":\n        edit(folders, docs, users)\n    elif choice == \"6\":\n        show_audit(folders, docs, users)\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "perms.eml",
      "eml": "# P109 permissions - roles as sets of actions, folders that grant roles to\n# users and can deny actions, and documents that inherit from every folder\n# above them. As in the corpus case permission-set-algebra, comparing two\n# roles is a subset question and the answer can be \"neither\": a partial\n# order. As in the corpus case inherited-permission-after-move, a\n# document keeps its resolved grants and refreshes them on a write, and a\n# move is not a write.\n#\n# Lists stand in for sets, always reported in a fixed order.\n\ndef actions():\n    return [\"read\", \"write\", \"export\", \"delete\", \"share\"]\n\ndef role_names():\n    return [\"viewer\", \"editor\", \"auditor\", \"owner\"]\n\ndef roles():\n    return {\"viewer\": [\"read\"], \"editor\": [\"read\", \"write\"], \"auditor\": [\"read\", \"export\"], \"owner\": [\"read\", \"write\", \"export\", \"delete\", \"share\"]}\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef subset(a, b):\n    for x in a:\n        if not has(b, x):\n            return False\n    return True\n\ndef ordered(xs):\n    # actions in the fixed order\n    [] => out\n    for a in actions():\n        if has(xs, a):\n            out + [a] => out\n    return out\n\ndef compare(a, b):\n    # how role a stands to role b: \"same\", \"below\", \"above\" or \"neither\"\n    roles() => r\n    subset(r[a], r[b]) => ab\n    subset(r[b], r[a]) => ba\n    if ab and ba:\n        return \"same\"\n    if ab:\n        return \"below\"\n    if ba:\n        return \"above\"\n    return \"neither\"\n\ndef covers():\n    # [lower, upper]: upper holds lower and more, with no role in between\n    [] => out\n    role_names() => names\n    for a in names:\n        for b in names:\n            if compare(a, b) == \"below\":\n                True => direct\n                for c in names:\n                    if compare(a, c) == \"below\" and compare(c, b) == \"below\":\n                        False => direct\n                if direct:\n                    out + [[a, b]] => out\n    return out\n\ndef sort_by_less(xs):\n    # an insertion sort that trusts \"a is below b\": with roles that are\n    # neither, the result follows the order they came in\n    [] => out\n    for x in xs:\n        out + [x] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and compare(out[j], out[j - 1]) == \"below\":\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n\ndef chain(folders, name):\n    # the folder and every folder above it, nearest first\n    [] => out\n    name => f\n    while f != \"\":\n        out + [f] => out\n        folders[f][0] => f\n    return out\n\ndef effective(folders, folder, user):\n    # [actions allowed, [[action, why], ...]]: every role granted to the\n    # user on this folder or above, minus every action denied there; a deny\n    # wins over any grant\n    roles() => r\n    [] => allowed\n    [] => denied\n    [] => why\n    for f in chain(folders, folder):\n        folders[f][1] => grants\n        if user in grants:\n            for a in r[grants[user]]:\n                if not has(allowed, a):\n                    allowed + [a] => allowed\n                    why + [[a, grants[user] + \" on \" + f]] => why\n        folders[f][2] => denies\n        if user in denies:\n            for a in denies[user]:\n                if not has(denied, a):\n                    denied + [a] => denied\n    [] => out\n    [] => reasons\n    for a in actions():\n        if has(denied, a):\n            reasons + [[a, \"denied\"]] => reasons\n        elif has(allowed, a):\n            out + [a] => out\n            for w in why:\n                if w[0] == a:\n                    reasons + [[a, w[1]]] => reasons\n    return [out, reasons]\n\ndef refresh(folders, docs, name, users):\n    # store what the document's folder gives each user now\n    {} => cache\n    for u in users:\n        effective(folders, docs[name][0], u)[0] => cache[u]\n    cache => docs[name][1]\n\ndef audit(folders, docs, users):\n    # [[doc, user, action, \"over\" or \"under\"], ...] where the cached grants\n    # differ from what the folder gives now: \"over\" when the cache allows\n    # and the folder does not\n    [] => out\n    for d in sorted_docs(docs):\n        for u in users:\n            docs[d][1][u] => cached\n            effective(folders, docs[d][0], u)[0] => now\n            for a in actions():\n                if has(cached, a) and not has(now, a):\n                    out + [[d, u, a, \"over\"]] => out\n                elif has(now, a) and not has(cached, a):\n                    out + [[d, u, a, \"under\"]] => out\n    return out\n\ndef sorted_docs(docs):\n    [] => out\n    for d in docs:\n        out + [d] => out\n    for i in [1:len(out) - 1]:\n        i => j\n        while j > 0 and out[j] < out[j - 1]:\n            out[j] => t\n            out[j - 1] => out[j]\n            t => out[j - 1]\n            j - 1 => j\n    return out\n",
      "python": "def actions():\n    return [\"read\", \"write\", \"export\", \"delete\", \"share\"]\n\ndef role_names():\n    return [\"viewer\", \"editor\", \"auditor\", \"owner\"]\n\ndef roles():\n    return {\"viewer\": [\"read\"], \"editor\": [\"read\", \"write\"], \"auditor\": [\"read\", \"export\"], \"owner\": [\"read\", \"write\", \"export\", \"delete\", \"share\"]}\n\ndef has(xs, x):\n    for y in xs:\n        if y == x:\n            return True\n    return False\n\ndef subset(a, b):\n    for x in a:\n        if not has(b, x):\n            return False\n    return True\n\ndef ordered(xs):\n    out = []\n    for a in actions():\n        if has(xs, a):\n            out = out + [a]\n    return out\n\ndef compare(a, b):\n    r = roles()\n    ab = subset(r[a], r[b])\n    ba = subset(r[b], r[a])\n    if ab and ba:\n        return \"same\"\n    if ab:\n        return \"below\"\n    if ba:\n        return \"above\"\n    return \"neither\"\n\ndef covers():\n    out = []\n    names = role_names()\n    for a in names:\n        for b in names:\n            if compare(a, b) == \"below\":\n                direct = True\n                for c in names:\n                    if compare(a, c) == \"below\" and compare(c, b) == \"below\":\n                        direct = False\n                if direct:\n                    out = out + [[a, b]]\n    return out\n\ndef sort_by_less(xs):\n    out = []\n    for x in xs:\n        out = out + [x]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and compare(out[j], out[j - 1]) == \"below\":\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n\ndef chain(folders, name):\n    out = []\n    f = name\n    while f != \"\":\n        out = out + [f]\n        f = folders[f][0]\n    return out\n\ndef effective(folders, folder, user):\n    r = roles()\n    allowed = []\n    denied = []\n    why = []\n    for f in chain(folders, folder):\n        grants = folders[f][1]\n        if user in grants:\n            for a in r[grants[user]]:\n                if not has(allowed, a):\n                    allowed = allowed + [a]\n                    why = why + [[a, grants[user] + \" on \" + f]]\n        denies = folders[f][2]\n        if user in denies:\n            for a in denies[user]:\n                if not has(denied, a):\n                    denied = denied + [a]\n    out = []\n    reasons = []\n    for a in actions():\n        if has(denied, a):\n            reasons = reasons + [[a, \"denied\"]]\n        elif has(allowed, a):\n            out = out + [a]\n            for w in why:\n                if w[0] == a:\n                    reasons = reasons + [[a, w[1]]]\n    return [out, reasons]\n\ndef refresh(folders, docs, name, users):\n    cache = {}\n    for u in users:\n        cache[u] = effective(folders, docs[name][0], u)[0]\n    docs[name][1] = cache\n\ndef audit(folders, docs, users):\n    out = []\n    for d in sorted_docs(docs):\n        for u in users:\n            cached = docs[d][1][u]\n            now = effective(folders, docs[d][0], u)[0]\n            for a in actions():\n                if has(cached, a) and not has(now, a):\n                    out = out + [[d, u, a, \"over\"]]\n                elif has(now, a) and not has(cached, a):\n                    out = out + [[d, u, a, \"under\"]]\n    return out\n\ndef sorted_docs(docs):\n    out = []\n    for d in docs:\n        out = out + [d]\n    for i in range(1, len(out)):\n        j = i\n        while j > 0 and out[j] < out[j - 1]:\n            t = out[j]\n            out[j] = out[j - 1]\n            out[j - 1] = t\n            j = j - 1\n    return out\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n3\n\n3\nzed\n3\nana\n\n3\nana\nx.doc\n4\n\n4\nx.doc\n4\nplan.txt\n\n4\nplan.txt\nhome\n4\nplan.txt\nteam\n5\n\n5\nx.doc\n7\n",
      "screen": "== Permissions ==\nRoles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> \nCancelled.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> zed\nType ana, ben, cy, dee or guest.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> ana\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> \nCancelled.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> ana\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> x.doc\nPick one of the documents listed.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> \nCancelled.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> x.doc\nPick one of the documents listed.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> plan.txt\nto folder (company, team, legal or public)> \nCancelled.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> plan.txt\nto folder (company, team, legal or public)> home\nType company, team, legal or public.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> plan.txt\nto folder (company, team, legal or public)> team\nplan.txt is already in team.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 5\ndocument> \nCancelled.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 5\ndocument> x.doc\nPick one of the documents listed.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\n2\n3\nben\ncontract.pdf\n3\ncy\ncontract.pdf\n3\ndee\nplan.txt\n3\nguest\nplan.txt\n6\n4\ncontract.pdf\npublic\n4\nbrochure.pdf\nlegal\n2\n3\nguest\ncontract.pdf\n3\nguest\nbrochure.pdf\n6\n5\ncontract.pdf\n6\n7\n",
      "screen": "== Permissions ==\nRoles as sets of actions, folders that grant roles and deny actions, and documents that inherit from every folder above them.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 1\n  viewer   read\n  editor   read, write\n  auditor  read, export\n  owner    read, write, export, delete, share\n  viewer < editor: every action of viewer is in editor\n  viewer < auditor: every action of viewer is in auditor\n  viewer < owner: every action of viewer is in owner\n  editor and auditor: neither - each has an action the other lacks, so <, <=, > and >= are all false\n  editor < owner: every action of editor is in owner\n  auditor < owner: every action of auditor is in owner\nDirectly above one another: viewer -> editor, viewer -> auditor, editor -> owner, auditor -> owner.\nSorted by <, from two starting orders: viewer, editor, auditor, owner / viewer, auditor, editor, owner. Roles that are neither keep whatever order they came in.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 2\n  company    ana owner, ben viewer, cy viewer\n    team     ben editor, dee editor\n      documents: notes.txt, plan.txt\n    legal    cy auditor; deny ben read\n      documents: contract.pdf\n    public   guest viewer\n      documents: brochure.pdf\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> ben\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf\nben on contract.pdf (in legal): nothing.\n  read   denied\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> cy\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf\ncy on contract.pdf (in legal): read, export.\n  read   auditor on legal\n  export auditor on legal\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> dee\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> plan.txt\ndee on plan.txt (in team): read, write.\n  read   editor on team\n  write  editor on team\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> guest\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> plan.txt\nguest on plan.txt (in team): nothing.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 6\nEvery stored grant matches what the folders give now.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> contract.pdf\nto folder (company, team, legal or public)> public\ncontract.pdf moved from legal to public. Its stored grants were not refreshed: a move changes the folder, not the document.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 4\ndocument> brochure.pdf\nto folder (company, team, legal or public)> legal\nbrochure.pdf moved from public to legal. Its stored grants were not refreshed: a move changes the folder, not the document.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 2\n  company    ana owner, ben viewer, cy viewer\n    team     ben editor, dee editor\n      documents: notes.txt, plan.txt\n    legal    cy auditor; deny ben read\n      documents: brochure.pdf\n    public   guest viewer\n      documents: contract.pdf\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> guest\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> contract.pdf\nguest on contract.pdf (in public): read.\n  read   viewer on public\n  The grants stored on the document still say: nothing.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 3\nuser (ana, ben, cy, dee or guest)> guest\ndocument (brochure.pdf, contract.pdf, notes.txt, plan.txt)> brochure.pdf\nguest on brochure.pdf (in legal): nothing.\n  The grants stored on the document still say: read.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 6\n  brochure.pdf ben   read   stored yes, folder no - more than it should\n  brochure.pdf cy    export stored no, folder yes - less than it should\n  brochure.pdf guest read   stored yes, folder no - more than it should\n  contract.pdf ben   read   stored no, folder yes - less than it should\n  contract.pdf cy    export stored yes, folder no - more than it should\n  contract.pdf guest read   stored no, folder yes - less than it should\n6 stored grants are out of date: 3 give more than the folder does, 3 less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 5\ndocument> contract.pdf\ncontract.pdf written: its stored grants were refreshed from public.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 6\n  brochure.pdf ben   read   stored yes, folder no - more than it should\n  brochure.pdf cy    export stored no, folder yes - less than it should\n  brochure.pdf guest read   stored yes, folder no - more than it should\n3 stored grants are out of date: 2 give more than the folder does, 1 less. Too little shows itself, as a refusal someone runs into; too much shows nothing at all.\n\n1) roles  2) folders  3) what a user may do  4) move a document  5) write a document  6) audit  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "permission-set-algebra",
      "caseId": "180-permission-set-algebra",
      "title": "For sets, `<` means subset"
    },
    {
      "slug": "inherited-permission-after-move",
      "caseId": "298-inherited-permission-after-move",
      "title": "Inherited permission after move — the cache refreshes on write, and a move is not a write"
    }
  ],
  "updated": "2026-10-16"
}
