diff --git a/Makefile b/Makefile index f3bec387d..d1f05b7cc 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,9 @@ load-rock: $(eval jimm_version := $(shell cat ./rocks/jimm.yaml | yq ".version")) @sudo /snap/rockcraft/current/bin/skopeo --insecure-policy copy oci-archive:jimm_${jimm_version}_amd64.rock docker-daemon:jimm:latest +test-auth-model: + fga model test --tests ./openfga/tests.fga.yaml + define check_dep if ! which $(1) > /dev/null; then\ echo "$(2)";\ @@ -127,6 +130,8 @@ endef APT_BASED := $(shell command -v apt-get >/dev/null; echo $$?) sys-deps: ifeq ($(APT_BASED),0) +# fga is required for openfga tests + @$(call check_deps,fga,Missing FGA client - install via 'go install github.com/openfga/cli/cmd/fga@latest') # golangci-lint is necessary for linting. @$(call check_dep,golangci-lint,Missing Golangci-lint - install from https://golangci-lint.run/welcome/install/ or 'sudo snap install golangci-lint --classic') # Go acts as the test runner. diff --git a/openfga/README.md b/openfga/README.md new file mode 100644 index 000000000..c0fee2ede --- /dev/null +++ b/openfga/README.md @@ -0,0 +1,32 @@ +# openfga + +## Introduction +The OpenFGA package holds our authorisation model and a go embed to pass the auth model into tests. +It also holds +tests to ensure the authorisation model is working correctly. + +## Requirements + +### VSCode Extension +Name: OpenFGA +Id: openfga.openfga-vscode +Description: Language support for OpenFGA authorization models +Version: 0.2.24 +Publisher: OpenFGA +VS Marketplace Link: https://marketplace.visualstudio.com/items?itemName=openfga.openfga-vscode + +### OpenFGA CLI +go install github.com/openfga/cli/cmd/fga@latest + +## Adding / modifying [to] the authorsation model +1. Open the authorisation_model.fga +2. Make your modification +3. Open the Command Pallette using Ctrl+Shift+P (Windows) or Command+Shift+P (OSX) +4. Select OpenFGA: Transform DSL to JSON +5. Save the file over the existing authorisation_model.json +6. Add tests to tests.fga.yaml - Learn more [here](https://openfga.dev/docs/modeling/testing) +7. Run them via: `make test-auth-model` + +## Test Structure +In order to avoid the potential entanglement of separate tests the tuples are artifically split into groups using this naming convention: (type):(2-letter test name)-(type)-(id) +The GitHub action supports running all tests in a directory, but keeping them in a single file improves the local development experience because the CLI does not. \ No newline at end of file diff --git a/openfga/authorisation_model.fga b/openfga/authorisation_model.fga index 793151ccf..be4668616 100644 --- a/openfga/authorisation_model.fga +++ b/openfga/authorisation_model.fga @@ -20,10 +20,6 @@ type controller define audit_log_viewer: [user, user:*, group#member] or administrator define controller: [controller] -type group - relations - define member: [user, user:*, group#member] - type model relations define administrator: [user, user:*, group#member] or administrator from controller @@ -31,8 +27,16 @@ type model define reader: [user, user:*, group#member] or writer define writer: [user, user:*, group#member] or administrator -type user - type serviceaccount relations define administrator: [user, user:*, group#member] + +type user + +type role + relations + define assignee: [user, user:*, group#member] + +type group + relations + define member: [user, user:*, group#member] diff --git a/openfga/authorisation_model.json b/openfga/authorisation_model.json index 7787b5ff0..362553c60 100644 --- a/openfga/authorisation_model.json +++ b/openfga/authorisation_model.json @@ -1,448 +1,477 @@ { - "schema_version": "1.1", - "type_definitions": [ - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "consumer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "model": { - "directly_related_user_types": [ - { - "type": "model" - } - ] - }, - "reader": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } + "schema_version": "1.1", + "type_definitions": [ + { + "type": "applicationoffer", + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "model" + } } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "model" - } - } - } - ] - } - }, - "consumer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "model": { - "this": {} - }, - "reader": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "consumer" - } - } - ] - } - } - }, - "type": "applicationoffer" + } + ] + } }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "can_addmodel": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - } - } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "can_addmodel": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "controller": { - "this": {} + "consumer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" } - }, - "type": "cloud" + } + ] + } + }, + "model": { + "this": {} }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "audit_log_viewer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - } + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "consumer" } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "audit_log_viewer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } - }, - "controller": { - "this": {} + } + ] + } + } + }, + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "consumer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "model": { + "directly_related_user_types": [ + { + "type": "model" + } + ] + }, + "reader": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + } + } + } + }, + { + "type": "cloud", + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } } - }, - "type": "controller" + } + ] + } }, - { - "metadata": { - "relations": { - "member": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } + "can_addmodel": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" } - }, - "relations": { - "member": { - "this": {} + } + ] + } + }, + "controller": { + "this": {} + } + }, + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "can_addmodel": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + } + } + } + }, + { + "type": "controller", + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } } - }, - "type": "group" + } + ] + } }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "controller": { - "directly_related_user_types": [ - { - "type": "controller" - } - ] - }, - "reader": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - }, - "writer": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } + "audit_log_viewer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" } - }, - "relations": { - "administrator": { - "union": { - "child": [ - { - "this": {} - }, - { - "tupleToUserset": { - "computedUserset": { - "relation": "administrator" - }, - "tupleset": { - "relation": "controller" - } - } - } - ] - } - }, - "controller": { - "this": {} - }, - "reader": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "writer" - } - } - ] - } - }, - "writer": { - "union": { - "child": [ - { - "this": {} - }, - { - "computedUserset": { - "relation": "administrator" - } - } - ] - } + } + ] + } + }, + "controller": { + "this": {} + } + }, + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "audit_log_viewer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + } + } + } + }, + { + "type": "model", + "relations": { + "administrator": { + "union": { + "child": [ + { + "this": {} + }, + { + "tupleToUserset": { + "computedUserset": { + "relation": "administrator" + }, + "tupleset": { + "relation": "controller" + } } - }, - "type": "model" + } + ] + } }, - { - "type": "user" + "controller": { + "this": {} }, - { - "metadata": { - "relations": { - "administrator": { - "directly_related_user_types": [ - { - "type": "user" - }, - { - "type": "user", - "wildcard": {} - }, - { - "relation": "member", - "type": "group" - } - ] - } + "reader": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "writer" } - }, - "relations": { - "administrator": { - "this": {} + } + ] + } + }, + "writer": { + "union": { + "child": [ + { + "this": {} + }, + { + "computedUserset": { + "relation": "administrator" } - }, - "type": "serviceaccount" + } + ] + } + } + }, + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "controller": { + "directly_related_user_types": [ + { + "type": "controller" + } + ] + }, + "reader": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + }, + "writer": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + } + } + } + }, + { + "type": "serviceaccount", + "relations": { + "administrator": { + "this": {} + } + }, + "metadata": { + "relations": { + "administrator": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + } + } + } + }, + { + "type": "user", + "relations": {}, + "metadata": null + }, + { + "type": "role", + "relations": { + "assignee": { + "this": {} + } + }, + "metadata": { + "relations": { + "assignee": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + } + } + } + }, + { + "type": "group", + "relations": { + "member": { + "this": {} + } + }, + "metadata": { + "relations": { + "member": { + "directly_related_user_types": [ + { + "type": "user" + }, + { + "type": "user", + "wildcard": {} + }, + { + "type": "group", + "relation": "member" + } + ] + } } - ] -} + } + } + ] +} \ No newline at end of file diff --git a/openfga/tests.fga.yaml b/openfga/tests.fga.yaml index eabcaba86..1512d4d9e 100644 --- a/openfga/tests.fga.yaml +++ b/openfga/tests.fga.yaml @@ -1,12 +1,5 @@ -# OpenFGA CLI instructions -# -# Installation: https://github.com/openfga/cli?tab=readme-ov-file#installation -# Command: fga model test --tests tests.fga.yaml - model_file: ./authorisation_model.fga -# In order to avoid the potential entanglement of separate tests the tuples are artifically split into groups using this naming convention: (type):(2-letter test name)-(type)-(id) -# The GitHub action supports running all tests in a directory, but keeping them in a single file improves the local development experience because the CLI does not. tuples: # Group (gr) - user: user:gr-user-1 @@ -19,6 +12,37 @@ tuples: relation: member object: group:gr-group-3 + # Role (ro) + # User to role direct + - user: user:ro-user-1 + relation: assignee + object: role:ro-role-1 + + # User to role via group + - user: user:ro-user-2 + relation: member + object: group:ro-group-1 + + - user: group:ro-group-1#member + relation: assignee + object: role:ro-role-2 + + # Wildcard user to role + - user: user:* + relation: assignee + object: role:ro-role-3 + + # Many roles one user + - user: user:ro-user-3 + relation: assignee + object: role:ro-role-4 + - user: user:ro-user-3 + relation: assignee + object: role:ro-role-5 + - user: user:ro-user-3 + relation: assignee + object: role:ro-role-6 + # Controller (co) - user: user:co-user-1 relation: administrator @@ -166,6 +190,38 @@ tests: - group:gr-group-1 - group:gr-group-2 - group:gr-group-3 + + # Ensures: + # - User can directly become an assignee of a role + # - User may go through a group to be indirectly an assignee + # - Wildcarded user can have direct assignee of a role + # - User can hold many roles + - name: Role + check: + - user: user:ro-user-1 + object: role:ro-role-1 + assertions: + assignee: true + + - user: user:ro-user-2 + object: role:ro-role-2 + assertions: + assignee: true + + - user: user:ro-user-3 + object: role:ro-role-3 + assertions: + assignee: true + + list_objects: + - user: user:ro-user-3 + type: role + assertions: + assignee: + - role:ro-role-3 # From user:* + - role:ro-role-4 + - role:ro-role-5 + - role:ro-role-6 # Checks whether: # - all or invididual users, or group members can become administators and audit_log_viewers of a controller