diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 76ccc21..23bc879 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,3 +24,78 @@ jobs: uses: ./ with: test_path: ./example/model.fga.yaml + + test_conditions_support: + name: Test conditions support + runs-on: ubuntu-latest + strategy: + matrix: + test: + - openfga_tag: v1.5.3 + conditions_supported: true + - openfga_tag: v1.4.3 + conditions_supported: true + - openfga_tag: v1.3.7 + conditions_supported: false + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: openfga + POSTGRES_PASSWORD: "1234" + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + OPENFGA_DATASTORE_ENGINE: 'postgres' + OPENFGA_DATASTORE_URI: 'postgres://openfga:1234@127.0.0.1:5432/openfga' + OPENFGA_LOG_LEVEL: debug + steps: + - uses: actions/checkout@v4 + - name: Install OpenFGA server ${{ matrix.test.openfga_tag }} + uses: jaxxstorm/action-install-gh-release@v1.11.0 + with: + repo: openfga/openfga + tag: ${{ matrix.test.openfga_tag }} + cache: enable + - name: Migrate OpenFGA Database + shell: bash + run: openfga migrate + - name: Start OpenFGA Server + shell: bash + run: openfga run & + - name: Install OpenFGA cli + uses: jaxxstorm/action-install-gh-release@v1.11.0 + with: + repo: openfga/cli + cache: enable + - name: Install jq + uses: dcarbone/install-jq-action@v2 + - name: Create store with model + id: 'store' + run: | + fga store create --model ./example/model_with_conditions.fga > store_response.json + cat store_response.json + store_id=$(jq -r '.store.id' store_response.json) + echo "store_id=${store_id}" >> $GITHUB_OUTPUT + - name: Run OpenFGA CLI Tests + id: 'tests' + uses: ./ + continue-on-error: true + with: + test_path: ./example/model_with_conditions.fga.yaml + fga_server_url: 'http://localhost:8080' + fga_server_store_id: ${{ steps.store.outputs.store_id }} + - name: Assert expected results + run: | + if [ "${{ matrix.test.conditions_supported }}" == "true" ] && [ "${{ steps.tests.outcome }}" == "failure" ] + then + echo "${{ matrix.test.openfga_tag }} is expected to support conditions but tests failed" + exit 1 + fi + if [ "${{ matrix.test.conditions_supported }}" == "false" ] && [ "${{ steps.tests.outcome }}" == "success" ] + then + echo "${{ matrix.test.openfga_tag }} is expected to not support conditions but tests passed" + exit 1 + fi + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..440c543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ + + +# IntelliJ IDEA +.idea +*.iml \ No newline at end of file diff --git a/README.md b/README.md index edc2e74..035b61f 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,23 @@ # OpenFGA Github Action - Test -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test?ref=badge_shield) +[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test?ref=badge_shield) This action can be used to test your authorization model using store test files. ## Parameter -| Parameter | Description | Required | Default | -|----------------------|----------------------------------------------------------------------------------|----------|--------------| -| `test_path` | The path to your store test file or folder relative to the root of your project. | No | `.` | -| `test_files_pattern` | The pattern to match test files. | No | `*.fga.yaml` | +| Parameter | Description | Required | Default | +|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|--------------| +| `test_path` | The path to your store test file or folder relative to the root of your project. | No | `.` | +| `test_files_pattern` | The pattern to match test files. | No | `*.fga.yaml` | +| `fga_server_url` | The OpenFGA server to test the Authorization Model against. If empty (which is the default value), the tests are run using the cli built-in OpenFGA instance. If specified, it is mandatory to specify the store id with the `fga_server_store_id` input, also the `model` and `model_file` entries of the tests are ignored | No | _empty_ | +| `fga_server_store_id` | The OpenFGA server store id. Must be provided if fga_server_url is configured. | No | _empty_ | +| `fga_api_token` | The api token to use for testing against an OpenFGA server. Ignored if `fga_server_url` is not provided. | No | _empty_ | > Note: the action will fail if no test is found in the specified test path with the given pattern - ## Examples - ### Running tests of `*.fga.yaml` files present in the repository ```yaml @@ -69,6 +70,65 @@ jobs: test_path: example/model.fga.yaml ``` +### Running tests against a given version of OpenFGA + +```yaml +name: Test Action + +on: + workflow_dispatch: + +jobs: + test: + name: Run test + runs-on: ubuntu-latest + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: openfga + POSTGRES_PASSWORD: '1234' + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + OPENFGA_DATASTORE_ENGINE: 'postgres' + OPENFGA_DATASTORE_URI: 'postgres://openfga:1234@127.0.0.1:5432/openfga' + OPENFGA_LOG_LEVEL: debug + steps: + - name: Install OpenFGA server v1.5.3 + uses: jaxxstorm/action-install-gh-release@v1.11.0 + with: + repo: openfga/openfga + tag: v1.5.3 + cache: enable + - name: Migrate OpenFGA database + shell: bash + run: openfga migrate + - name: Start OpenFGA server in background + shell: bash + run: openfga run & + - name: Install OpenFGA cli + uses: jaxxstorm/action-install-gh-release@v1.11.0 + with: + repo: openfga/cli + cache: enable + - name: Install jq + uses: dcarbone/install-jq-action@v2 + - name: Create store with model + id: 'store' + run: | + fga store create --model ./example/model_with_conditions.fga > store_response.json + cat store_response.json + store_id= $(jq -r '.store.id' store_response.json) + echo "store_id=${store_id}" >> $GITHUB_OUTPUT + - name: Run tests + uses: openfga/action-openfga-test@v0.1 + with: + fga_server_url: 'http://localhost:8080' + fga_server_store_id: ${{ steps.store.outputs.store_id }} +``` ## License + [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fopenfga%2Faction-openfga-test?ref=badge_large) diff --git a/action.yml b/action.yml index 5c01ebe..6ab2930 100644 --- a/action.yml +++ b/action.yml @@ -13,6 +13,19 @@ inputs: description: 'Pattern to match the test files' required: false default: '*.fga.yaml' + fga_server_url: + description: 'The OpenFGA server to test the Authorization Model against. If not provided, the tests will be run using the cli built-in OpenFGA instance.' + required: false + default: '' + fga_server_store_id: + description: 'The OpenFGA server store id. Must be provided if fga_server_url is configured.' + required: false + default: '' + fga_api_token: + description: 'The api token to use for testing against an OpenFGA server. Ignored if fga_server_url is not provided.' + required: false + default: '' + runs: using: composite @@ -22,17 +35,35 @@ runs: with: repo: openfga/cli cache: enable + - uses: chrisdickinson/setup-yq@v1.0.1 + with: + yq-version: v4.25.3 - name: Run OpenFGA CLI shell: bash run: | - while IFS= read -r -d '' test_file - do - ((test_files_count+=1)) + while IFS= read -r -d '' test_file + do + ((test_files_count+=1)) + + if [[ -z "${{ inputs.fga_server_url }}" ]]; then echo "Running FGA test file ${test_file}" fga model test --tests "${test_file}" - done < <(find ${{ inputs.test_path }} -name "${{ inputs.test_files_pattern }}" -print0) - - if [[ ${test_files_count} -eq 0 ]]; then - echo "No FGA test file found for path '${{ inputs.test_path }}' and pattern '${{ inputs.test_files_pattern }}'" - exit 1 - fi \ No newline at end of file + else + echo "Running FGA test file ${test_file} against OpenFGA server ${{ inputs.fga_server_url }}" + fga_token="${{ inputs.fga_api_token }}" + test_file_without_model=mktemp + yq 'del(.model_file, .model)' ${test_file} > ${test_file_without_model} + fga model test \ + ${fga_server_opts} \ + --api-url "${{ inputs.fga_server_url }}" \ + --store-id "${{ inputs.fga_server_store_id }}" \ + ${fga_token:+--api-token ${fga_token}} \ + --tests "${test_file_without_model}" + fi + + done < <(find ${{ inputs.test_path }} -name "${{ inputs.test_files_pattern }}" -print0) + + if [[ ${test_files_count} -eq 0 ]]; then + echo "No FGA test file found for path '${{ inputs.test_path }}' and pattern '${{ inputs.test_files_pattern }}'" + exit 1 + fi \ No newline at end of file diff --git a/example/model_with_conditions.fga b/example/model_with_conditions.fga new file mode 100644 index 0000000..be929ae --- /dev/null +++ b/example/model_with_conditions.fga @@ -0,0 +1,12 @@ +model + schema 1.1 + +type user + +type document + relations + define viewer: [user, user with non_expired_grant] + +condition non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) { + current_time < grant_time + grant_duration +} \ No newline at end of file diff --git a/example/model_with_conditions.fga.yaml b/example/model_with_conditions.fga.yaml new file mode 100644 index 0000000..89991c5 --- /dev/null +++ b/example/model_with_conditions.fga.yaml @@ -0,0 +1,64 @@ +name: FolderBox with temporal accesses # store name +model_file: ./model_with_conditions.fga + +tuples: + - user: user:bob + relation: viewer + object: document:1 + + - user: user:anne + relation: viewer + object: document:1 + condition: + name: non_expired_grant + context: + grant_time : "2023-01-01T00:00:00Z" + grant_duration : 1h + + - user: user:anne + relation: viewer + object: document:2 + condition: + name: non_expired_grant + context: + grant_time : "2023-01-01T00:00:00Z" + grant_duration : 5s + +tests: + - name: Test temporal access + check: + - user: user:anne + object: document:1 + context: + current_time: "2023-01-01T00:10:00Z" + assertions: + viewer: true + + - user: user:anne + object: document:1 + context: + current_time: "2023-01-01T02:00:00Z" + assertions: + viewer: false + + - user: user:anne + object: document:2 + context: + current_time: "2023-01-01T00:00:09Z" + assertions: + viewer: false + + - user: user:bob + object: document:1 + assertions: + viewer: true + + list_objects: + - user: user:anne + type: document + context: + current_time: "2023-01-01T00:00:01Z" + assertions: + viewer: + - document:1 + - document:2 \ No newline at end of file