├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── conf.default.yml ├── gateway ├── gateway.go ├── gateway_test.go ├── launcher.go └── launcher_test.go ├── go.mod ├── go.sum ├── main.go ├── sam.yml └── test ├── test.go └── testdata ├── checks ├── check_resource_batch │ ├── crb_case_00.yaml │ ├── crb_case_01.yaml │ ├── crb_invalid_case_00.yaml │ ├── crb_invalid_case_01.yaml │ └── crb_invalid_case_02.yaml └── check_resource_set │ ├── crs_case_00.yaml │ ├── crs_case_01.yaml │ ├── crs_case_02.yaml │ ├── crs_case_03.yaml │ ├── crs_case_04.yaml │ ├── crs_case_05.yaml │ ├── crs_case_06.yaml │ ├── crs_case_07.yaml │ ├── crs_case_08.yaml │ ├── crs_invalid_case_00.yaml │ └── crs_invalid_case_01.yaml ├── conf.yml ├── store ├── derived_roles │ ├── common_roles.yaml │ ├── derived_roles_01.yaml │ ├── derived_roles_02.yaml │ └── derived_roles_03.yaml ├── principal_policies │ └── policy_01.yaml └── resource_policies │ ├── policy_01.yaml │ ├── policy_02.yaml │ ├── policy_03.yaml │ └── policy_04.yaml └── verify_key.jwk /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .cerbos 15 | 16 | dist 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | run: 3 | timeout: 300s 4 | skip-dirs: 5 | - .cerbos 6 | build-tags: 7 | - tests 8 | 9 | linters-settings: 10 | exhaustive: 11 | default-signifies-exhaustive: true 12 | 13 | gci: 14 | local-prefixes: github.com/cerbos/cerbos-aws-lambda 15 | 16 | gofumpt: 17 | extra-rules: true 18 | 19 | goheader: 20 | values: 21 | const: 22 | COMPANY: Zenauth Ltd. 23 | template: |- 24 | Copyright {{ YEAR-RANGE }} {{ COMPANY }} 25 | SPDX-License-Identifier: Apache-2.0 26 | 27 | nolintlint: 28 | allow-unused: false 29 | allow-leading-space: false 30 | require-specific: true 31 | 32 | tagliatelle: 33 | case: 34 | rules: 35 | json: goCamel 36 | yaml: goCamel 37 | xml: goCamel 38 | bson: goCamel 39 | 40 | linters: 41 | enable: 42 | - asciicheck 43 | - bodyclose 44 | - dupl 45 | - durationcheck 46 | - errorlint 47 | - exhaustive 48 | - exportloopref 49 | - forbidigo 50 | - forcetypeassert 51 | - gci 52 | - goconst 53 | - gocritic 54 | - godot 55 | - gofumpt 56 | - goimports 57 | - goheader 58 | - gomnd 59 | - gosec 60 | - ifshort 61 | - importas 62 | - makezero 63 | - misspell 64 | - nakedret 65 | - nestif 66 | - nilerr 67 | - noctx 68 | - nolintlint 69 | - prealloc 70 | - predeclared 71 | - promlinter 72 | - revive 73 | - rowserrcheck 74 | - sqlclosecheck 75 | - tagliatelle 76 | - thelper 77 | - tparallel 78 | - unconvert 79 | - unparam 80 | - whitespace 81 | disable: 82 | - cyclop 83 | - depguard 84 | - dogsled 85 | - exhaustivestruct 86 | - funlen 87 | - gochecknoglobals 88 | - gochecknoinits 89 | - gocognit 90 | - gocyclo 91 | - godox 92 | - goerr113 93 | - gofmt 94 | - golint 95 | - gomodguard 96 | - goprintffuncname 97 | - interfacer 98 | - lll 99 | - maligned 100 | - nlreturn 101 | - paralleltest 102 | - stylecheck 103 | - testpackage 104 | - wastedassign 105 | - wrapcheck 106 | - wsl 107 | 108 | issues: 109 | max-same-issues: 30 110 | 111 | fix: true 112 | 113 | exclude-rules: 114 | - path: _test\.go 115 | linters: 116 | - goconst 117 | - gomnd 118 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest as build 2 | 3 | WORKDIR /src 4 | 5 | COPY gateway ./gateway 6 | COPY go.mod go.sum main.go ./ 7 | 8 | 9 | RUN go get -d -v ./... 10 | RUN CGO_ENABLED=0 go build -ldflags '-s -w' -o /gw main.go 11 | RUN chmod +x /gw 12 | 13 | # Now copy it into our base image. 14 | FROM gcr.io/distroless/base 15 | ARG ARCH=x86_64 16 | COPY --from=build /gw / 17 | COPY .cerbos/Linux_${ARCH}/cerbos / 18 | COPY conf.default.yml /conf.yml 19 | 20 | # Uncomment for testing with a disk storage 21 | # define REMOTE_CERBOS_URL pointing to HTTP API (see CerbosServerFunctionAPI in sam.yml) 22 | # then run `make test` 23 | #COPY test/testdata/store /store 24 | #COPY test/testdata/conf.yml test/testdata/verify_key.jwk / 25 | ENTRYPOINT ["/gw"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | .PHONY: all 4 | all: clean build 5 | 6 | .PHONY: build 7 | build: cerbos-binary image publish-image publish-lambda update-lambda 8 | 9 | .PHONY: clean 10 | clean: 11 | @ rm -rf .cerbos 12 | 13 | .PHONY: cerbos-binary 14 | cerbos-binary: 15 | @ if [[ "$$CERBOS_RELEASE" ]]; then \ 16 | CURRENT_RELEASE=$$CERBOS_RELEASE; \ 17 | else \ 18 | CURRENT_RELEASE=$$(curl -sH "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/cerbos/cerbos/tags?per_page=1" | jq -r '.[].name | ltrimstr("v")'); \ 19 | fi; \ 20 | ver='[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+'; \ 21 | if [[ $$CURRENT_RELEASE =~ $$ver ]]; then \ 22 | CURRENT_RELEASE="$${BASH_REMATCH[0]}"; \ 23 | else \ 24 | echo "Unexpected format of CERBOS_RELEASE ($$CURRENT_RELEASE), expected semantic version 'x.x.x'" >&2; \ 25 | exit 1; \ 26 | fi; \ 27 | echo "Using Cerbos $$CURRENT_RELEASE"; \ 28 | arch=$$(uname -m); [ "$$arch" != "x86_64" ] && [ "$$arch" != "arm64" ] && { echo "$${arch} - unsupported architecture, supported: x86_64, arm64" >&2; exit 1; }; \ 29 | oses=(Linux); \ 30 | if [[ $$(uname -s) = Darwin ]]; then \ 31 | oses+=(Darwin); \ 32 | fi; \ 33 | for os in "$${oses[@]}"; do \ 34 | a=$$arch; \ 35 | if [ "$$a" = "amd64" ]; then \ 36 | a=x86_64; \ 37 | fi; \ 38 | p=$${os}_$${a}; \ 39 | mkdir -p ./.cerbos/$${p}; \ 40 | if [[ ! -e "./.cerbos/$${p}/cerbos" ]]; then \ 41 | echo "Downloading Cerbos binary for $${os}"; \ 42 | curl -sL "https://github.com/cerbos/cerbos/releases/download/v$${CURRENT_RELEASE}/cerbos_$${CURRENT_RELEASE}_$${os}_$${a}.tar.gz" | tar -xz -C ./.cerbos/$${p}/ cerbos; \ 43 | fi; \ 44 | done; \ 45 | 46 | .PHONY: image 47 | image: cerbos-binary 48 | @ arch=$$(uname -m); [ "$$arch" != "x86_64" ] && [ "$$arch" != "arm64" ] && { echo "$${arch} - unsupported architecture, supported: x86_64, arm64" >&2; exit 1; }; \ 49 | docker build --build-arg ARCH=$$arch -t cerbos/aws-lambda-gateway . 50 | 51 | .PHONY: ecr 52 | ecr: 53 | @ [ -n "$$ECR_REPOSITORY_URL" ] || { echo "Please set ECR_REPOSITORY_URL environment variable" >&2; exit 1; } 54 | 55 | .PHONY: publish-image 56 | publish-image: image ecr 57 | docker tag cerbos/aws-lambda-gateway:latest $${ECR_REPOSITORY_URL}:latest 58 | docker push $${ECR_REPOSITORY_URL}:latest 59 | 60 | .PHONY: publish-lambda 61 | publish-lambda: ecr 62 | @ arch=$$(uname -m); [ "$$arch" != "x86_64" ] && [ "$$arch" != "arm64" ] && { echo "$${arch} - unsupported architecture, supported: x86_64, arm64" >&2; exit 1; }; \ 63 | sam deploy --template sam.yml --stack-name $${CERBOS_STACK_NAME:-Cerbos} --resolve-image-repos \ 64 | --capabilities CAPABILITY_IAM --no-confirm-changeset --no-fail-on-empty-changeset --parameter-overrides ArchitectureParameter=$$arch 65 | 66 | .PHONY: update-lambda 67 | update-lambda: ecr 68 | fn=$$(aws cloudformation list-stack-resources --stack-name $${CERBOS_STACK_NAME:-Cerbos} --query "StackResourceSummaries[?ResourceType=='AWS::Lambda::Function'].PhysicalResourceId" --output text); \ 69 | aws lambda update-function-code --function-name $$fn --image-uri $${ECR_REPOSITORY_URL}:latest > /dev/null; \ 70 | aws lambda wait function-updated --function-name $$fn 71 | 72 | 73 | .PHONY: test 74 | test: 75 | go test -v ./... 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cerbos AWS Lambda Docker Image 2 | ============================== 3 | Gateway service implements AWS Lambda runtime and invokes Cerbos server API hosted in the same AWS Lambda instance. 4 | 5 | Cerbos is the open core, language-agnostic, scalable authorization solution that makes user permissions and authorization simple to implement and manage by writing context-aware access control policies for your application resources. 6 | 7 | * [Cerbos website](https://cerbos.dev) 8 | * [Cerbos documentation](https://docs.cerbos.dev) 9 | * [Cerbos GitHub repository](https://github.com/cerbos/cerbos) 10 | * [Cerbos Slack community](http://go.cerbos.io/slack) 11 | 12 | ## Description 13 | This project builds a docker image that can be used to run a Cerbos server in AWS Lambda. The images will contain the gateway executable and the Cerbos binary. 14 | 15 | The following commands assume you run a Unix-like system with x86_64 or arm64 architectures. 16 | 17 | There's also an example of AWS Lambda function based on this image. The function is built using [AWS SAM](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) model. 18 | 19 | ### Prerequisites 20 | 21 | The following tools are required: 22 | - Make - build automation tool 23 | - AWS CLI 24 | - AWS SAM CLI - if you wish to use the provided AWS Lambda function template 25 | - Docker 26 | - jq - If you want the Makefile to automatically detect the version of Cerbos. Otherwise use the `CERBOS_RELEASE` environment variable to specify the Cerbos version to use. 27 | 28 | 29 | ### Build the Docker image 30 | 31 | Check out `conf.default.yml` for Cerbos configuration. The default configuration uses blob storage, e.g. AWS S3 bucket. Cerbos config can read from environment variables. If you choose to do so, your AWS Lambda has to expose them. 32 | 33 | By default, the latest release of Cerbos is used. If you want to use a particular Cerbos version, you can specify it in `CERBOS_RELEASE` environment variable. 34 | 35 | Run the following command to build the docker image 'cerbos/aws-lambda-gateway': 36 | ```shell 37 | make image 38 | ``` 39 | 40 | Note that the image will be built in whatever architecture you are running on (x86 or arm64) - the AWS region you use must support the architecture you are deploying too also else you will get an exec format error when it tries to start up the lambda. 41 | 42 | ### Publish the Docker image 43 | 44 | To publish the image, you will need to have an AWS ECR repository. You can create one in the AWS console or using AWS CLI with the following command (replace `` with the name of your repository): 45 | 46 | You will see `repositoryUri` in the output of the command. Save it for later use. 47 | ```shell 48 | aws ecr create-repository --repository-name --image-scanning-configuration scanOnPush=true 49 | ``` 50 | 51 | Then you will need to get an authentication token for the repository. You can do it with the following command: 52 | 53 | ```shell 54 | export ECR_REPOSITORY_URL= 55 | aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPOSITORY_URL 56 | ``` 57 | 58 | Now you can publish the image with the following command: 59 | ```shell 60 | make publish-image 61 | ``` 62 | 63 | ### Create AWS Lambda function 64 | You can create an AWS Lambda function referencing the published image with any tool. Alternatively, you can use the provided template `sam.yml`. For the latter option, please visit the template and replace `` with the value you saved in the previous step. The template exposes these environment variables: 65 | - BUCKET_URL - the URL of the S3 bucket where Cerbos policies are stored. 66 | - BUCKET_PREFIX - optional prefix for the S3 bucket. 67 | - CERBOS_LOGGING_LEVEL - Cerbos logging level. It defaults to INFO. 68 | 69 | You will need to grant the role access to the S3 bucket you are storing policies in. 70 | 71 | To publish the function, run the following command: 72 | ```shell 73 | make publish-lambda 74 | ``` 75 | 76 | The command will create an AWS Lambda function as part of the stack called as per `CERBOS_STACK_NAME` environment variable (if unset, defaults to `Cerbos`). The stack will also create API Gateway resources and an IAM role for the function. **Ensure the role has the necessary permissions to access the S3 bucket (or other policy storage you might use)**. 77 | 78 | Should you change the configuration and rebuild the image, you can update the Lambda via: 79 | 80 | ```shell 81 | make clean 82 | make image 83 | make publish-image 84 | make update-lambda 85 | ``` 86 | -------------------------------------------------------------------------------- /conf.default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://docs.cerbos.dev/cerbos/latest/configuration/auxdata.html 3 | # WARNING: Disabling JWT verification is not recommended because it makes the system 4 | # insecure by forcing Cerbos to evaluate policies using potentially tampered data. 5 | auxData: 6 | jwt: 7 | disableVerification: true 8 | 9 | storage: 10 | driver: "blob" 11 | blob: 12 | bucket: $BUCKET_URL 13 | prefix: $BUCKET_PREFIX 14 | workDir: /tmp/workDir 15 | updatePollInterval: 15s -------------------------------------------------------------------------------- /gateway/gateway.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package gateway 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "log" 14 | "mime" 15 | "net/http" 16 | "net/url" 17 | "strconv" 18 | "strings" 19 | 20 | "github.com/aws/aws-lambda-go/events" 21 | "go.uber.org/multierr" 22 | ) 23 | 24 | var ErrNotStarted = errors.New("timeout exceeded starting Cerbos") 25 | 26 | type processManager interface { 27 | StartProcess(ctx context.Context, cerbos, workDir, configFile string) error 28 | Started() bool 29 | StopProcess() error 30 | } 31 | 32 | type Gateway struct { 33 | httpClient *http.Client 34 | processManager 35 | cerbosAddress *url.URL 36 | } 37 | 38 | // NewGateway creates a new Gateway instance. 39 | func NewGateway(addr string) (*Gateway, error) { 40 | if addr == "" { 41 | return nil, errors.New("cerbos address not provided") 42 | } 43 | u, err := url.Parse(addr) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to parse cerbos address: %w", err) 46 | } 47 | gw := &Gateway{ 48 | cerbosAddress: u, 49 | httpClient: &http.Client{}, 50 | } 51 | 52 | gw.processManager = newLauncher(gw.httpClient, gw.healthEndpoint()) 53 | 54 | return gw, nil 55 | } 56 | 57 | func (g *Gateway) healthEndpoint() string { 58 | return g.cerbosAddress.String() + "/_cerbos/health" 59 | } 60 | 61 | func (g *Gateway) Invoke(ctx context.Context, payload []byte) ([]byte, error) { 62 | if !g.Started() { 63 | panic("cerbos process not started") 64 | } 65 | var evt events.APIGatewayV2HTTPRequest 66 | if err := json.Unmarshal(payload, &evt); err != nil { 67 | return []byte{}, err 68 | } 69 | 70 | r, err := g.newRequest(ctx, evt) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | resp, err := g.httpClient.Do(r) 76 | if err != nil { 77 | err := fmt.Errorf("error calling HTTP endpoint %q: %w", r.URL.String(), err) 78 | log.Print(err) 79 | return nil, err 80 | } 81 | 82 | res, err := MkGatewayResponse(resp) 83 | if err != nil { 84 | log.Print(err) 85 | res = &events.APIGatewayV2HTTPResponse{ 86 | StatusCode: http.StatusInternalServerError, 87 | Body: err.Error(), 88 | Headers: map[string]string{"content-type": "text/plain; charset=utf-8"}, 89 | } 90 | } 91 | return json.Marshal(res) 92 | } 93 | 94 | // newRequest returns a new http.Request from the given Lambda event. 95 | func (g *Gateway) newRequest(ctx context.Context, e events.APIGatewayV2HTTPRequest) (*http.Request, error) { 96 | // path 97 | u, err := url.Parse(e.RawPath) 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to parse RawPath %q: %w", e.RawPath, err) 100 | } 101 | 102 | u.RawQuery = e.RawQueryString 103 | 104 | u.Scheme = g.cerbosAddress.Scheme 105 | u.Host = g.cerbosAddress.Host 106 | 107 | // base64 encoded body 108 | body := e.Body 109 | if e.IsBase64Encoded { 110 | b, err := base64.StdEncoding.DecodeString(body) 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to base64 decode body: %w", err) 113 | } 114 | body = string(b) 115 | } 116 | 117 | req, err := http.NewRequestWithContext(ctx, e.RequestContext.HTTP.Method, u.String(), strings.NewReader(body)) 118 | if err != nil { 119 | return nil, fmt.Errorf("failed to create HTTP request: %w", err) 120 | } 121 | 122 | // remote addr 123 | req.RemoteAddr = e.RequestContext.HTTP.SourceIP 124 | 125 | // header fields 126 | for k, values := range e.Headers { 127 | for _, v := range strings.Split(values, ",") { 128 | req.Header.Add(k, v) 129 | } 130 | } 131 | for _, c := range e.Cookies { 132 | req.Header.Add("Cookie", c) 133 | } 134 | 135 | // content-length 136 | if req.Header.Get("Content-Length") == "" && body != "" { 137 | req.Header.Set("Content-Length", strconv.Itoa(len(body))) 138 | } 139 | 140 | // custom fields 141 | req.Header.Set("X-Request-Id", e.RequestContext.RequestID) 142 | req.Header.Set("X-Stage", e.RequestContext.Stage) 143 | 144 | // xray support 145 | if traceID := ctx.Value("x-amzn-trace-id"); traceID != nil { 146 | req.Header.Set("X-Amzn-Trace-Id", fmt.Sprintf("%v", traceID)) 147 | } 148 | 149 | // host 150 | req.Host = req.URL.Host 151 | 152 | return req, nil 153 | } 154 | 155 | func MkGatewayResponse(hresp *http.Response) (res *events.APIGatewayV2HTTPResponse, err error) { 156 | res = new(events.APIGatewayV2HTTPResponse) 157 | res.Headers = make(map[string]string) 158 | 159 | defer multierr.AppendInvoke(&err, multierr.Close(hresp.Body)) 160 | body, err := io.ReadAll(hresp.Body) 161 | res.IsBase64Encoded, err = isBinary(hresp.Header) 162 | if err != nil { 163 | return nil, err 164 | } 165 | if res.IsBase64Encoded { 166 | res.Body = base64.StdEncoding.EncodeToString(body) 167 | } else { 168 | res.Body = string(body) 169 | } 170 | // copy headers 171 | for k, vv := range hresp.Header { 172 | res.Headers[strings.ToLower(k)] = strings.Join(vv, ",") 173 | } 174 | // see https://aws.amazon.com/blogs/compute/simply-serverless-using-aws-lambda-to-expose-custom-cookies-with-api-gateway/ 175 | res.Cookies = hresp.Header["Set-Cookie"] 176 | res.StatusCode = hresp.StatusCode 177 | return res, nil 178 | } 179 | 180 | // isBinary checks content type of the returns true if it describes binary data 181 | // It uses a non-exhaustive list of binary content types. 182 | func isBinary(h http.Header) (bool, error) { 183 | kind := h.Get("Content-Type") 184 | t, _, err := mime.ParseMediaType(kind) 185 | if err != nil { 186 | return false, fmt.Errorf("failed to parse media type %q: %w", kind, err) 187 | } 188 | return strings.HasPrefix(t, "image") || 189 | strings.HasPrefix(t, "gzip") || 190 | t == "application/octet-stream", nil 191 | } 192 | -------------------------------------------------------------------------------- /gateway/gateway_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package gateway 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/fs" 12 | "os" 13 | "path/filepath" 14 | "runtime" 15 | "strings" 16 | "testing" 17 | 18 | "github.com/aws/aws-lambda-go/events" 19 | "github.com/ghodss/yaml" 20 | "github.com/google/go-cmp/cmp" 21 | "github.com/google/go-cmp/cmp/cmpopts" 22 | "github.com/stretchr/testify/require" 23 | 24 | "github.com/cerbos/cerbos-aws-lambda/test" 25 | ) 26 | 27 | // implement process manager interface. 28 | type testProcessManager struct{} 29 | 30 | func (testProcessManager) StartProcess(_ context.Context, _, _, _ string) error { return nil } 31 | 32 | func (testProcessManager) Started() bool { return true } 33 | 34 | func (testProcessManager) StopProcess() error { return nil } 35 | 36 | const localCerbosURL = "http://127.0.0.1" + httpListenAddr 37 | 38 | var remoteCerbosURL = os.Getenv("REMOTE_CERBOS_URL") 39 | 40 | func TestGateway_Invoke(t *testing.T) { 41 | ctx := context.Background() 42 | is := require.New(t) 43 | tests := loadTests(t, "checks/check_resource_set", "checks/check_resource_batch") 44 | is.NotEmpty(tests) 45 | 46 | cerbosURL := localCerbosURL 47 | if remoteCerbosURL != "" { 48 | cerbosURL = remoteCerbosURL 49 | } 50 | gw, err := NewGateway(cerbosURL) 51 | if remoteCerbosURL != "" { 52 | gw.processManager = testProcessManager{} 53 | } 54 | is.NoError(err) 55 | err = gw.StartProcess(ctx, pathToCerbos(t), test.PathToDir(t, ""), "conf.yml") 56 | is.NoError(err) 57 | defer func() { 58 | is.NoError(gw.StopProcess()) 59 | }() 60 | for file, tt := range tests { 61 | batch := false 62 | resources := tt.CheckResourceSet 63 | if strings.HasSuffix(filepath.Dir(file), "batch") { 64 | batch = true 65 | resources = tt.CheckResourceBatch 66 | } 67 | t.Run(filepath.Base(file)+":"+tt.Description, func(t *testing.T) { 68 | is := require.New(t) 69 | input, err := json.Marshal(resources.Input) 70 | is.NoError(err) 71 | 72 | endpoint := "/api/check" 73 | if batch { 74 | endpoint = "/api/check_resource_batch" 75 | } 76 | payload, err := mkPayload(input, endpoint) 77 | is.NoError(err) 78 | 79 | got, err := gw.Invoke(ctx, payload) 80 | is.NoError(err) 81 | 82 | var response events.APIGatewayV2HTTPResponse 83 | err = json.Unmarshal(got, &response) 84 | is.NoError(err) 85 | if tt.WantError { 86 | is.NotEqual(response.StatusCode, 200) 87 | } 88 | is.Equal(tt.WantStatus.HTTPStatusCode, response.StatusCode, struct { 89 | request, response string 90 | }{request: string(input), response: response.Body}) 91 | if resources.WantResponse != nil { 92 | body := readBody(t, response.Body, batch) 93 | is.NoError(err) 94 | diff := cmp.Diff(resources.WantResponse, body, 95 | cmpopts.EquateEmpty(), cmpopts.SortSlices(func(a, b interface{}) bool { 96 | if a, ok := a.(string); ok { 97 | if b, ok := b.(string); ok { 98 | return a < b 99 | } 100 | } 101 | return false 102 | })) 103 | is.Empty(diff, "mismatch: -want +got") 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func readBody(t *testing.T, body string, batch bool) (res map[string]interface{}) { 110 | t.Helper() 111 | 112 | var v interface{} 113 | var err error 114 | if batch { 115 | var output CheckResourceBatchResponse 116 | err = json.Unmarshal([]byte(body), &output) 117 | v = output 118 | } else { 119 | var output CheckResourceSetResponse 120 | err = json.Unmarshal([]byte(body), &output) 121 | v = output 122 | } 123 | require.NoError(t, err) 124 | b, err := json.Marshal(v) 125 | require.NoError(t, err) 126 | body = string(b) 127 | err = json.Unmarshal([]byte(body), &res) 128 | require.NoError(t, err) 129 | return 130 | } 131 | 132 | func pathToCerbos(t *testing.T) string { 133 | t.Helper() 134 | goOS := os.Getenv("GOOS") 135 | if goOS == "" { 136 | goOS = runtime.GOOS 137 | } 138 | goARCH := os.Getenv("GOARCH") 139 | if goARCH == "" { 140 | goARCH = runtime.GOARCH 141 | } 142 | arch := goARCH 143 | if arch == "amd64" { 144 | arch = "x86_64" 145 | } 146 | path := filepath.Join(test.PathToDir(t, ""), 147 | "../../.cerbos", 148 | fmt.Sprintf("%s_%s", strings.Title(goOS), arch), 149 | "cerbos") 150 | 151 | stat, err := os.Stat(path) 152 | require.NoError(t, err) 153 | require.True(t, !stat.IsDir()) 154 | 155 | return path 156 | } 157 | 158 | func mkPayload(input []byte, endpoint string) ([]byte, error) { 159 | var request events.APIGatewayV2HTTPRequest 160 | request.RawPath = "http://example.com" + endpoint 161 | request.RequestContext.HTTP.Method = "POST" 162 | request.Headers = map[string]string{ 163 | "content-type": "application/json", 164 | } 165 | request.Body = string(input) 166 | return json.Marshal(request) 167 | } 168 | 169 | func loadTests(t *testing.T, dirs ...string) map[string]checkResources { 170 | t.Helper() 171 | tests := make(map[string]checkResources) 172 | 173 | for _, dir := range dirs { 174 | err := filepath.WalkDir(test.PathToDir(t, dir), func(path string, info fs.DirEntry, err error) error { 175 | if err != nil { 176 | return err 177 | } 178 | if info.IsDir() { 179 | return nil 180 | } 181 | if filepath.Ext(path) != ".yaml" { 182 | return nil 183 | } 184 | // read contents of file 185 | file, err := os.Open(path) 186 | if err != nil { 187 | return err 188 | } 189 | defer file.Close() 190 | content, err := io.ReadAll(file) 191 | if err != nil { 192 | return err 193 | } 194 | test := checkResources{} 195 | err = yaml.Unmarshal(content, &test) 196 | if err != nil { 197 | return err 198 | } 199 | tests[path] = test 200 | return nil 201 | }) 202 | require.NoError(t, err) 203 | } 204 | return tests 205 | } 206 | 207 | type checkResources struct { 208 | Description string `json:"description"` 209 | WantError bool `json:"wantError"` 210 | WantStatus struct { 211 | HTTPStatusCode int `json:"httpStatusCode"` 212 | } `json:"wantStatus"` 213 | CheckResourceSet struct { 214 | Input map[string]interface{} `json:"input"` 215 | WantResponse map[string]interface{} `json:"wantResponse"` 216 | } `json:"checkResourceSet"` 217 | CheckResourceBatch struct { 218 | Input map[string]interface{} `json:"input"` 219 | WantResponse map[string]interface{} `json:"wantResponse"` 220 | } `json:"checkResourceBatch"` 221 | } 222 | 223 | type CheckResourceSetResponse struct { 224 | RequestID string `json:"requestId"` //nolint:tagliatelle 225 | ResourceInstances map[string]*CheckResourceSetResponseActionEffectMap `json:"resourceInstances"` 226 | Meta *CheckResourceSetResponseMeta `json:"meta"` 227 | } 228 | 229 | type CheckResourceSetResponseActionEffectMap struct { 230 | Actions map[string]string `json:"actions"` 231 | } 232 | 233 | type CheckResourceSetResponseMeta struct { 234 | ResourceInstances map[string]*CheckResourceSetResponseMetaActionMeta `json:"resourceInstances"` 235 | } 236 | 237 | type CheckResourceSetResponseMetaActionMeta struct { 238 | Actions map[string]*CheckResourceSetResponseMetaEffectMeta `json:"actions"` 239 | EffectiveDerivedRoles []string `json:"effectiveDerivedRoles"` 240 | } 241 | 242 | type CheckResourceSetResponseMetaEffectMeta struct { 243 | MatchedPolicy string `json:"matchedPolicy"` 244 | } 245 | 246 | type CheckResourceBatchResponse struct { 247 | RequestID string `json:"requestId"` //nolint:tagliatelle 248 | Results []*CheckResourceBatchResponseActionEffectMap `json:"results"` 249 | } 250 | 251 | type CheckResourceBatchResponseActionEffectMap struct { 252 | ResourceID string `json:"resourceId"` //nolint:tagliatelle 253 | Actions map[string]string `json:"actions"` 254 | } 255 | -------------------------------------------------------------------------------- /gateway/launcher.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package gateway 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | const ( 16 | httpListenAddr = ":3592" 17 | grpcListenAddr = ":3593" 18 | 19 | cerbosLogLevelEnv = "CERBOS_LOG_LEVEL" 20 | cerbosLaunchTimeoutEnv = "CERBOS_LAUNCH_TIMEOUT" 21 | cerbosHealthCheckIntervalEnv = "CERBOS_HEALTH_CHECK_INTERVAL" 22 | 23 | cerbosLaunchTimeoutDefault = 2 * time.Second 24 | cerbosHealthCheckIntervalDefault = 50 * time.Millisecond 25 | ) 26 | 27 | type launcher struct { 28 | httpClient *http.Client 29 | process *os.Process 30 | healthEndpoint string 31 | } 32 | 33 | func newLauncher(httpClient *http.Client, healthEndpoint string) *launcher { 34 | return &launcher{ 35 | httpClient: httpClient, 36 | healthEndpoint: healthEndpoint, 37 | } 38 | } 39 | 40 | func (l *launcher) StartProcess(ctx context.Context, cerbos, workDir, configFile string) (err error) { 41 | if l.Started() { 42 | return nil 43 | } 44 | logLevel := os.Getenv(cerbosLogLevelEnv) 45 | if logLevel == "" { 46 | logLevel = "INFO" 47 | } 48 | 49 | timeout := parseDurationOrDefault(os.Getenv(cerbosLaunchTimeoutEnv), cerbosLaunchTimeoutDefault) 50 | log.Printf("cerbos launch timeout: %s", timeout) 51 | healthCheckInterval := parseDurationOrDefault(os.Getenv(cerbosHealthCheckIntervalEnv), cerbosHealthCheckIntervalDefault) 52 | log.Printf("health check interval: %s", healthCheckInterval) 53 | argv := []string{"cerbos", "server", "--config=" + configFile, "--log-level=" + logLevel, "--set=server.httpListenAddr=" + httpListenAddr, "--set=server.grpcListenAddr=" + grpcListenAddr} 54 | l.process, err = os.StartProcess(cerbos, argv, &os.ProcAttr{ 55 | Dir: workDir, 56 | Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 57 | }) 58 | if err != nil { 59 | return err 60 | } 61 | request, err := http.NewRequestWithContext(ctx, "GET", l.healthEndpoint, nil) 62 | if err != nil { 63 | return err 64 | } 65 | startTime := time.Now() 66 | for { 67 | time.Sleep(healthCheckInterval) 68 | resp, err := l.httpClient.Do(request) 69 | log.Printf("cerbos health check: %v, pid: %v", err, l.process.Pid) 70 | if resp != nil { 71 | resp.Body.Close() 72 | if resp.StatusCode == http.StatusOK { 73 | log.Printf("Cerbos server started in %s", time.Since(startTime)) 74 | go func() { 75 | ps, err := l.process.Wait() 76 | log.Printf("Cerbos process exited: state %q, err %q", ps.String(), err) 77 | l.process = nil 78 | os.Exit(1) 79 | }() 80 | return nil 81 | } 82 | } 83 | if time.Since(startTime) > timeout { 84 | break 85 | } 86 | } 87 | 88 | return fmt.Errorf("in %v: %w", time.Since(startTime), ErrNotStarted) 89 | } 90 | 91 | func parseDurationOrDefault(v string, d time.Duration) time.Duration { 92 | if v == "" { 93 | return d 94 | } 95 | res, err := time.ParseDuration(v) 96 | if err != nil { 97 | return d 98 | } 99 | return res 100 | } 101 | 102 | func (l *launcher) Started() bool { 103 | return l.process != nil 104 | } 105 | 106 | func (l *launcher) StopProcess() error { 107 | if l.process != nil { 108 | err := l.process.Kill() 109 | if err != nil { 110 | log.Printf("failed to kill cerbos process: %v", err) 111 | return err 112 | } 113 | l.process = nil 114 | } 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /gateway/launcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package gateway 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func Test_parseDurationOrDefault(t *testing.T) { 12 | defaultValue := time.Duration(time.Now().Second()) * time.Nanosecond 13 | tests := []struct { 14 | value string 15 | want time.Duration 16 | }{ 17 | // valid values 18 | {value: "10s", want: 10 * time.Second}, 19 | {value: "31ms", want: 31 * time.Millisecond}, 20 | // if invalid values return default 21 | {value: "", want: defaultValue}, 22 | {value: "hi", want: defaultValue}, 23 | {value: "-1", want: defaultValue}, 24 | {value: "0x123", want: defaultValue}, 25 | {value: "4ss", want: defaultValue}, 26 | } 27 | 28 | for _, tt := range tests { 29 | t.Run(tt.value, func(t *testing.T) { 30 | if got := parseDurationOrDefault(tt.value, defaultValue); got != tt.want { 31 | t.Errorf("parseDurationOrDefault() = %v, want %v", got, tt.want) 32 | } 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cerbos/cerbos-aws-lambda 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.41.0 7 | github.com/ghodss/yaml v1.0.0 8 | github.com/google/go-cmp v0.6.0 9 | github.com/stretchr/testify v1.8.4 10 | go.uber.org/multierr v1.11.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/kr/pretty v0.1.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 18 | gopkg.in/yaml.v2 v2.4.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-lambda-go v1.41.0 h1:l/5fyVb6Ud9uYd411xdHZzSf2n86TakxzpvIoz7l+3Y= 2 | github.com/aws/aws-lambda-go v1.41.0/go.mod h1:jwFe2KmMsHmffA1X2R09hH6lFzJQxzI8qK17ewzbQMM= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 7 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 11 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 12 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 13 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 19 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 20 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 21 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 22 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 23 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 24 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 25 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 26 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 27 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 30 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 32 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "log" 9 | 10 | "github.com/aws/aws-lambda-go/lambda" 11 | 12 | "github.com/cerbos/cerbos-aws-lambda/gateway" 13 | ) 14 | 15 | const cerbosHTTPAddr = "http://127.0.0.1:3592" 16 | 17 | func main() { 18 | gw, err := gateway.NewGateway(cerbosHTTPAddr) 19 | if err != nil { 20 | log.Print("failed to create a gateway") 21 | return 22 | } 23 | ctx := context.Background() 24 | err = gw.StartProcess(ctx, "cerbos", "", "conf.yml") 25 | if err != nil { 26 | log.Printf("Failed to start a process: %s", err) 27 | return 28 | } 29 | // start lambda handler 30 | lambda.StartHandler(gw) 31 | } 32 | -------------------------------------------------------------------------------- /sam.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Cerbos server 4 | 5 | # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst 6 | Globals: 7 | Function: 8 | Timeout: 5 9 | 10 | Parameters: 11 | ArchitectureParameter: 12 | Type: String 13 | Default: x86_64 14 | AllowedValues: 15 | - x86_64 16 | - arm64 17 | Description: "AWS Lambda function architecture: x86_64 or arm64." 18 | 19 | Resources: 20 | CerbosServerFunction: 21 | Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 22 | Properties: 23 | Architectures: 24 | - { Ref: ArchitectureParameter } 25 | PackageType: Image 26 | ImageUri: 414605243264.dkr.ecr.us-east-2.amazonaws.com/lambda:latest 27 | MemorySize: 1024 28 | Events: 29 | CatchAll: 30 | Type: HttpApi # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 31 | Properties: 32 | Path: /{proxy+} 33 | Method: ANY 34 | Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object 35 | Variables: 36 | BUCKET_URL: "" 37 | BUCKET_PREFIX: "" 38 | CERBOS_LOG_LEVEL: INFO 39 | XDG_CACHE_HOME: /tmp 40 | 41 | Outputs: 42 | # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function 43 | # Find out more about other implicit resources you can reference within SAM 44 | # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api 45 | CerbosServerFunctionAPI: 46 | Description: "API Gateway endpoint URL" 47 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" 48 | CerbosServerFunction: 49 | Description: "Cerbos Server Function ARN" 50 | Value: !GetAtt CerbosServerFunction.Arn 51 | HelloWorldFunctionIamRole: 52 | Description: "IAM Role created for the Cerbos Server function" 53 | Value: !GetAtt CerbosServerFunctionRole.Arn 54 | -------------------------------------------------------------------------------- /test/test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Zenauth Ltd. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package test 5 | 6 | import ( 7 | "path/filepath" 8 | "runtime" 9 | "testing" 10 | ) 11 | 12 | func PathToDir(tb testing.TB, dir string) string { 13 | tb.Helper() 14 | 15 | _, currFile, _, ok := runtime.Caller(0) 16 | if !ok { 17 | tb.Error("Failed to detect testdata directory") 18 | return "" 19 | } 20 | 21 | return filepath.Join(filepath.Dir(currFile), "testdata", dir) 22 | } 23 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_batch/crb_case_00.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Mixed batch" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceBatch: 6 | input: { 7 | "requestId": "test", 8 | "principal": { 9 | "id": "john", 10 | "policyVersion": "20210210", 11 | "roles": [ 12 | "employee" 13 | ], 14 | "attr": { 15 | "department": "marketing", 16 | "geography": "GB", 17 | "team": "design" 18 | } 19 | }, 20 | "resources": [ 21 | { 22 | "actions": [ 23 | "view:public", 24 | "approve" 25 | ], 26 | "resource": { 27 | "kind": "leave_request", 28 | "policyVersion": "20210210", 29 | "id": "XX125", 30 | "attr": { 31 | "department": "marketing", 32 | "geography": "GB", 33 | "id": "XX125", 34 | "owner": "john", 35 | "team": "design" 36 | } 37 | } 38 | }, 39 | { 40 | "actions": [ 41 | "view:public", 42 | "approve" 43 | ], 44 | "resource": { 45 | "kind": "leave_request", 46 | "policyVersion": "20210210", 47 | "id": "YY125", 48 | "attr": { 49 | "department": "engineering", 50 | "geography": "GB", 51 | "id": "YY125", 52 | "owner": "jenny", 53 | "team": "backend" 54 | } 55 | } 56 | }, 57 | { 58 | "actions": [ 59 | "view:public", 60 | "delete", 61 | "edit" 62 | ], 63 | "resource": { 64 | "kind": "salary_record", 65 | "policyVersion": "20210210", 66 | "id": "YY525", 67 | "attr": { 68 | "department": "engineering", 69 | "geography": "GB", 70 | "id": "YY525", 71 | "owner": "mark", 72 | "team": "backend" 73 | } 74 | } 75 | } 76 | ] 77 | } 78 | wantResponse: { 79 | "requestId": "test", 80 | "results": [ 81 | { 82 | "resourceId": "XX125", 83 | "actions": { 84 | "view:public": "EFFECT_ALLOW", 85 | "approve": "EFFECT_DENY" 86 | } 87 | }, 88 | { 89 | "resourceId": "YY125", 90 | "actions": { 91 | "view:public": "EFFECT_ALLOW", 92 | "approve": "EFFECT_DENY" 93 | } 94 | }, 95 | { 96 | "resourceId": "YY525", 97 | "actions": { 98 | "view:public": "EFFECT_DENY", 99 | "delete": "EFFECT_DENY", 100 | "edit": "EFFECT_DENY" 101 | } 102 | } 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_batch/crb_case_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Aux data" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceBatch: 6 | input: { 7 | "requestId": "test", 8 | "principal": { 9 | "id": "john", 10 | "policyVersion": "20210210", 11 | "roles": [ 12 | "employee" 13 | ], 14 | "attr": { 15 | "department": "marketing", 16 | "geography": "GB", 17 | "team": "design" 18 | } 19 | }, 20 | "resources": [ 21 | { 22 | "actions": [ 23 | "view:public", 24 | "approve", 25 | "defer" 26 | ], 27 | "resource": { 28 | "kind": "leave_request", 29 | "policyVersion": "20210210", 30 | "id": "XX125", 31 | "attr": { 32 | "department": "marketing", 33 | "geography": "GB", 34 | "id": "XX125", 35 | "owner": "john", 36 | "team": "design" 37 | } 38 | } 39 | } 40 | ], 41 | "auxData": { 42 | "jwt": { 43 | "token": "eyJhbGciOiJFUzM4NCIsImtpZCI6IjE5TGZaYXRFZGc4M1lOYzVyMjNndU1KcXJuND0iLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiY2VyYm9zLWp3dC10ZXN0cyJdLCJjdXN0b21BcnJheSI6WyJBIiwiQiIsIkMiXSwiY3VzdG9tSW50Ijo0MiwiY3VzdG9tTWFwIjp7IkEiOiJBQSIsIkIiOiJCQiIsIkMiOiJDQyJ9LCJjdXN0b21TdHJpbmciOiJmb29iYXIiLCJleHAiOjE5NDk5MzQwMzksImlzcyI6ImNlcmJvcy10ZXN0LXN1aXRlIn0.WN_tOScSpd_EI-P5EI1YlagxEgExSfBjAtcrgcF6lyWj1lGpR_GKx9goZEp2p_t5AVWXN_bjz_sMUmJdJa4cVd55Qm1miR-FKu6oNRHnSEWdMFmnArwPw-YDJWfylLFX" 44 | } 45 | } 46 | } 47 | wantResponse: { 48 | "requestId": "test", 49 | "results": [ 50 | { 51 | "resourceId": "XX125", 52 | "actions": { 53 | "view:public": "EFFECT_ALLOW", 54 | "approve": "EFFECT_DENY", 55 | "defer": "EFFECT_ALLOW" 56 | } 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_batch/crb_invalid_case_00.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Missing principal" 3 | wantStatus: 4 | httpStatusCode: 400 5 | wantError: true 6 | checkResourceBatch: 7 | input: { 8 | "requestId": "test", 9 | "resources": [ 10 | { 11 | "actions": [ 12 | "view:public", 13 | "approve" 14 | ], 15 | "resource": { 16 | "kind": "leave_request", 17 | "policyVersion": "20210210", 18 | "id": "XX125", 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "id": "XX125", 23 | "owner": "john", 24 | "team": "design" 25 | } 26 | } 27 | }, 28 | { 29 | "actions": [ 30 | "view:public", 31 | "approve" 32 | ], 33 | "resource": { 34 | "kind": "leave_request", 35 | "policyVersion": "20210210", 36 | "id": "YY125", 37 | "attr": { 38 | "department": "engineering", 39 | "geography": "GB", 40 | "id": "YY125", 41 | "owner": "jenny", 42 | "team": "backend" 43 | } 44 | } 45 | }, 46 | { 47 | "actions": [ 48 | "view:public", 49 | "delete", 50 | "edit" 51 | ], 52 | "resource": { 53 | "kind": "salary_record", 54 | "policyVersion": "20210210", 55 | "id": "YY525", 56 | "attr": { 57 | "department": "engineering", 58 | "geography": "GB", 59 | "id": "YY525", 60 | "owner": "mark", 61 | "team": "backend" 62 | } 63 | } 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_batch/crb_invalid_case_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Empty actions" 3 | wantStatus: 4 | httpStatusCode: 400 5 | wantError: true 6 | checkResourceBatch: 7 | input: { 8 | "requestId": "test", 9 | "principal": { 10 | "id": "john", 11 | "policyVersion": "20210210", 12 | "roles": [ 13 | "employee" 14 | ], 15 | "attr": { 16 | "department": "marketing", 17 | "geography": "GB", 18 | "team": "design" 19 | } 20 | }, 21 | "resources": [ 22 | { 23 | "resource": { 24 | "kind": "leave_request", 25 | "policyVersion": "20210210", 26 | "id": "XX125", 27 | "attr": { 28 | "department": "marketing", 29 | "geography": "GB", 30 | "id": "XX125", 31 | "owner": "john", 32 | "team": "design" 33 | } 34 | } 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_batch/crb_invalid_case_02.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Empty resources list" 3 | wantStatus: 4 | httpStatusCode: 400 5 | wantError: true 6 | checkResourceBatch: 7 | input: { 8 | "requestId": "test", 9 | "principal": { 10 | "id": "john", 11 | "policyVersion": "20210210", 12 | "roles": [ 13 | "employee" 14 | ], 15 | "attr": { 16 | "department": "marketing", 17 | "geography": "GB", 18 | "team": "design" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_00.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "No metadata" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "actions": [ 9 | "view:public", 10 | "approve" 11 | ], 12 | "principal": { 13 | "id": "john", 14 | "policyVersion": "20210210", 15 | "roles": [ 16 | "employee" 17 | ], 18 | "attr": { 19 | "department": "marketing", 20 | "geography": "GB", 21 | "team": "design" 22 | } 23 | }, 24 | "resource": { 25 | "kind": "leave_request", 26 | "policyVersion": "20210210", 27 | "instances": { 28 | "XX125": { 29 | "attr": { 30 | "department": "marketing", 31 | "geography": "GB", 32 | "id": "XX125", 33 | "owner": "john", 34 | "team": "design" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | wantResponse: { 41 | "meta": null, 42 | "requestId": "test", 43 | "resourceInstances": { 44 | "XX125": { 45 | "actions": { 46 | "view:public": "EFFECT_ALLOW", 47 | "approve": "EFFECT_DENY" 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "John tries to view and approve his own leave request" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "view:public", 11 | "approve" 12 | ], 13 | "principal": { 14 | "id": "john", 15 | "policyVersion": "20210210", 16 | "roles": [ 17 | "employee" 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "team": "design" 23 | } 24 | }, 25 | "resource": { 26 | "kind": "leave_request", 27 | "policyVersion": "20210210", 28 | "instances": { 29 | "XX125": { 30 | "attr": { 31 | "department": "marketing", 32 | "geography": "GB", 33 | "id": "XX125", 34 | "owner": "john", 35 | "team": "design" 36 | } 37 | } 38 | } 39 | } 40 | } 41 | wantResponse: { 42 | "requestId": "test", 43 | "resourceInstances": { 44 | "XX125": { 45 | "actions": { 46 | "view:public": "EFFECT_ALLOW", 47 | "approve": "EFFECT_DENY" 48 | } 49 | } 50 | }, 51 | "meta": { 52 | "resourceInstances": { 53 | "XX125": { 54 | "actions": { 55 | "view:public": { 56 | "matchedPolicy": "resource.leave_request.v20210210" 57 | }, 58 | "approve": { 59 | "matchedPolicy": "resource.leave_request.v20210210" 60 | } 61 | }, 62 | "effectiveDerivedRoles": ["employee_that_owns_the_record", "any_employee"] 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_02.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "John's direct manager tries to approve a draft leave request and a pending-approval leave request" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "approve", 11 | ], 12 | "principal": { 13 | "id": "sally", 14 | "policyVersion": "20210210", 15 | "roles": [ 16 | "employee", 17 | "manager" 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "team": "design", 23 | "managed_geographies": "GB" 24 | } 25 | }, 26 | "resource": { 27 | "kind": "leave_request", 28 | "policyVersion": "20210210", 29 | "instances": { 30 | "XX125": { 31 | "attr": { 32 | "department": "marketing", 33 | "geography": "GB", 34 | "id": "XX125", 35 | "owner": "john", 36 | "team": "design", 37 | "status": "PENDING_APPROVAL" 38 | } 39 | }, 40 | "XX150": { 41 | "attr": { 42 | "department": "marketing", 43 | "geography": "GB", 44 | "id": "XX150", 45 | "owner": "john", 46 | "team": "design", 47 | "status": "DRAFT" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | wantResponse: { 54 | "requestId": "test", 55 | "resourceInstances": { 56 | "XX125": { 57 | "actions": { 58 | "approve": "EFFECT_ALLOW" 59 | } 60 | }, 61 | "XX150": { 62 | "actions": { 63 | "approve": "EFFECT_DENY" 64 | } 65 | } 66 | }, 67 | "meta": { 68 | "resourceInstances": { 69 | "XX125": { 70 | "actions": { 71 | "approve": { 72 | "matchedPolicy": "resource.leave_request.v20210210" 73 | } 74 | }, 75 | "effectiveDerivedRoles": ["any_employee", "direct_manager"] 76 | }, 77 | "XX150": { 78 | "actions": { 79 | "approve": { 80 | "matchedPolicy": "resource.leave_request.v20210210" 81 | } 82 | }, 83 | "effectiveDerivedRoles": ["any_employee", "direct_manager"] 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_03.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "A different manager tries to approve a draft leave request and a pending-approval leave request" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "approve", 11 | ], 12 | "principal": { 13 | "id": "betty", 14 | "policyVersion": "20210210", 15 | "roles": [ 16 | "employee", 17 | "manager" 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "FR", 22 | "team": "design", 23 | "managed_geographies": "FR" 24 | } 25 | }, 26 | "resource": { 27 | "kind": "leave_request", 28 | "policyVersion": "20210210", 29 | "instances": { 30 | "XX125": { 31 | "attr": { 32 | "department": "marketing", 33 | "geography": "GB", 34 | "id": "XX125", 35 | "owner": "john", 36 | "team": "design", 37 | "status": "PENDING_APPROVAL" 38 | } 39 | }, 40 | "XX150": { 41 | "attr": { 42 | "department": "marketing", 43 | "geography": "GB", 44 | "id": "XX150", 45 | "owner": "john", 46 | "team": "design", 47 | "status": "DRAFT" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | wantResponse: { 54 | "requestId": "test", 55 | "resourceInstances": { 56 | "XX125": { 57 | "actions": { 58 | "approve": "EFFECT_DENY" 59 | } 60 | }, 61 | "XX150": { 62 | "actions": { 63 | "approve": "EFFECT_DENY" 64 | } 65 | } 66 | }, 67 | "meta": { 68 | "resourceInstances": { 69 | "XX125": { 70 | "actions": { 71 | "approve": { 72 | "matchedPolicy": "resource.leave_request.v20210210" 73 | } 74 | }, 75 | "effectiveDerivedRoles": ["any_employee"] 76 | }, 77 | "XX150": { 78 | "actions": { 79 | "approve": { 80 | "matchedPolicy": "resource.leave_request.v20210210" 81 | } 82 | }, 83 | "effectiveDerivedRoles": ["any_employee"] 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_04.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Donald Duck tries to view and approve a production leave request and a dev leave request" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "approve", 11 | "view:public" 12 | ], 13 | "principal": { 14 | "id": "donald_duck", 15 | "policyVersion": "20210210", 16 | "roles": [ 17 | "employee", 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "team": "design", 23 | "managed_geographies": "GB" 24 | } 25 | }, 26 | "resource": { 27 | "kind": "leave_request", 28 | "policyVersion": "20210210", 29 | "instances": { 30 | "XX125": { 31 | "attr": { 32 | "department": "marketing", 33 | "geography": "GB", 34 | "id": "XX125", 35 | "owner": "john", 36 | "team": "design", 37 | "status": "PENDING_APPROVAL", 38 | "dev_record": true 39 | } 40 | }, 41 | "XX150": { 42 | "attr": { 43 | "department": "marketing", 44 | "geography": "GB", 45 | "id": "XX150", 46 | "owner": "john", 47 | "team": "design", 48 | "status": "PENDING_APPROVAL" 49 | } 50 | } 51 | } 52 | } 53 | } 54 | wantResponse: { 55 | "requestId": "test", 56 | "resourceInstances": { 57 | "XX125": { 58 | "actions": { 59 | "approve": "EFFECT_ALLOW", 60 | "view:public": "EFFECT_ALLOW", 61 | } 62 | }, 63 | "XX150": { 64 | "actions": { 65 | "approve": "EFFECT_DENY", 66 | "view:public": "EFFECT_ALLOW", 67 | } 68 | } 69 | }, 70 | "meta": { 71 | "resourceInstances": { 72 | "XX125": { 73 | "actions": { 74 | "approve": { 75 | "matchedPolicy": "principal.donald_duck.v20210210" 76 | }, 77 | "view:public": { 78 | "matchedPolicy": "principal.donald_duck.v20210210" 79 | } 80 | }, 81 | "effectiveDerivedRoles": null 82 | }, 83 | "XX150": { 84 | "actions": { 85 | "approve": { 86 | "matchedPolicy": "resource.leave_request.v20210210" 87 | }, 88 | "view:public": { 89 | "matchedPolicy": "resource.leave_request.v20210210" 90 | } 91 | }, 92 | "effectiveDerivedRoles": ["any_employee"] 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_05.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Explicit deny from Donald Duck principal policy" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "view:public" 11 | ], 12 | "principal": { 13 | "id": "donald_duck", 14 | "policyVersion": "20210210", 15 | "roles": [ 16 | "employee", 17 | ], 18 | "attr": { 19 | "department": "marketing", 20 | "geography": "GB", 21 | "team": "design", 22 | "managed_geographies": "GB" 23 | } 24 | }, 25 | "resource": { 26 | "kind": "salary_record", 27 | "policyVersion": "20210210", 28 | "instances": { 29 | "XX125": { 30 | "attr": { 31 | "department": "marketing", 32 | "geography": "GB", 33 | "id": "XX125", 34 | "owner": "john", 35 | "team": "design", 36 | "dev_record": true 37 | } 38 | } 39 | } 40 | } 41 | } 42 | wantResponse: { 43 | "requestId": "test", 44 | "resourceInstances": { 45 | "XX125": { 46 | "actions": { 47 | "view:public": "EFFECT_DENY", 48 | } 49 | } 50 | }, 51 | "meta": { 52 | "resourceInstances": { 53 | "XX125": { 54 | "actions": { 55 | "view:public": { 56 | "matchedPolicy": "principal.donald_duck.v20210210" 57 | } 58 | }, 59 | "effectiveDerivedRoles": null 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_06.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "No policy match" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "includeMeta": true, 9 | "actions": [ 10 | "view:public", 11 | "approve" 12 | ], 13 | "principal": { 14 | "id": "john", 15 | "roles": [ 16 | "employee" 17 | ], 18 | "attr": { 19 | "department": "marketing", 20 | "geography": "GB", 21 | "team": "design" 22 | } 23 | }, 24 | "resource": { 25 | "kind": "leave_request", 26 | "instances": { 27 | "XX125": { 28 | "attr": { 29 | "department": "marketing", 30 | "geography": "GB", 31 | "id": "XX125", 32 | "owner": "john", 33 | "team": "design" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | wantResponse: { 40 | "requestId": "test", 41 | "resourceInstances": { 42 | "XX125": { 43 | "actions": { 44 | "view:public": "EFFECT_DENY", 45 | "approve": "EFFECT_DENY" 46 | } 47 | } 48 | }, 49 | "meta": { 50 | "resourceInstances": { 51 | "XX125": { 52 | "actions": { 53 | "view:public": { 54 | "matchedPolicy": "NO_MATCH" 55 | }, 56 | "approve": { 57 | "matchedPolicy": "NO_MATCH" 58 | } 59 | }, 60 | "effectiveDerivedRoles": null 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_07.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Donald Duck tries to view and approve a load of leave requests" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "actions": [ 9 | "approve", 10 | "view:public" 11 | ], 12 | "principal": { 13 | "id": "donald_duck", 14 | "policyVersion": "20210210", 15 | "roles": [ 16 | "employee", 17 | ], 18 | "attr": { 19 | "department": "marketing", 20 | "geography": "GB", 21 | "team": "design", 22 | "managed_geographies": "GB" 23 | } 24 | }, 25 | "resource": { 26 | "kind": "leave_request", 27 | "policyVersion": "20210210", 28 | "instances": { 29 | "XX100": { 30 | "attr": { 31 | "department": "marketing", 32 | "geography": "GB", 33 | "id": "XX100", 34 | "owner": "john", 35 | "team": "design", 36 | "status": "PENDING_APPROVAL", 37 | "dev_record": true 38 | } 39 | }, 40 | "XX101": { 41 | "attr": { 42 | "department": "marketing", 43 | "geography": "GB", 44 | "id": "XX101", 45 | "owner": "john", 46 | "team": "design", 47 | "status": "PENDING_APPROVAL", 48 | "dev_record": true 49 | } 50 | }, 51 | "XX102": { 52 | "attr": { 53 | "department": "marketing", 54 | "geography": "GB", 55 | "id": "XX102", 56 | "owner": "john", 57 | "team": "design", 58 | "status": "PENDING_APPROVAL", 59 | "dev_record": true 60 | } 61 | }, 62 | "XX103": { 63 | "attr": { 64 | "department": "marketing", 65 | "geography": "GB", 66 | "id": "XX103", 67 | "owner": "john", 68 | "team": "design", 69 | "status": "PENDING_APPROVAL", 70 | "dev_record": true 71 | } 72 | }, 73 | "XX104": { 74 | "attr": { 75 | "department": "marketing", 76 | "geography": "GB", 77 | "id": "XX104", 78 | "owner": "john", 79 | "team": "design", 80 | "status": "PENDING_APPROVAL", 81 | "dev_record": true 82 | } 83 | }, 84 | "XX105": { 85 | "attr": { 86 | "department": "marketing", 87 | "geography": "GB", 88 | "id": "XX105", 89 | "owner": "john", 90 | "team": "design", 91 | "status": "PENDING_APPROVAL", 92 | "dev_record": true 93 | } 94 | }, 95 | "XX106": { 96 | "attr": { 97 | "department": "marketing", 98 | "geography": "GB", 99 | "id": "XX106", 100 | "owner": "john", 101 | "team": "design", 102 | "status": "PENDING_APPROVAL", 103 | "dev_record": true 104 | } 105 | }, 106 | "XX107": { 107 | "attr": { 108 | "department": "marketing", 109 | "geography": "GB", 110 | "id": "XX107", 111 | "owner": "john", 112 | "team": "design", 113 | "status": "PENDING_APPROVAL", 114 | "dev_record": true 115 | } 116 | }, 117 | "XX108": { 118 | "attr": { 119 | "department": "marketing", 120 | "geography": "GB", 121 | "id": "XX108", 122 | "owner": "john", 123 | "team": "design", 124 | "status": "PENDING_APPROVAL", 125 | "dev_record": true 126 | } 127 | }, 128 | "XX109": { 129 | "attr": { 130 | "department": "marketing", 131 | "geography": "GB", 132 | "id": "XX109", 133 | "owner": "john", 134 | "team": "design", 135 | "status": "PENDING_APPROVAL", 136 | "dev_record": true 137 | } 138 | }, 139 | "XX110": { 140 | "attr": { 141 | "department": "marketing", 142 | "geography": "GB", 143 | "id": "XX110", 144 | "owner": "john", 145 | "team": "design", 146 | "status": "PENDING_APPROVAL", 147 | "dev_record": true 148 | } 149 | }, 150 | "XX200": { 151 | "attr": { 152 | "department": "marketing", 153 | "geography": "GB", 154 | "id": "XX200", 155 | "owner": "john", 156 | "team": "design", 157 | "status": "PENDING_APPROVAL" 158 | } 159 | }, 160 | "XX201": { 161 | "attr": { 162 | "department": "marketing", 163 | "geography": "GB", 164 | "id": "XX201", 165 | "owner": "john", 166 | "team": "design", 167 | "status": "PENDING_APPROVAL" 168 | } 169 | }, 170 | "XX202": { 171 | "attr": { 172 | "department": "marketing", 173 | "geography": "GB", 174 | "id": "XX202", 175 | "owner": "john", 176 | "team": "design", 177 | "status": "PENDING_APPROVAL" 178 | } 179 | }, 180 | "XX203": { 181 | "attr": { 182 | "department": "marketing", 183 | "geography": "GB", 184 | "id": "XX203", 185 | "owner": "john", 186 | "team": "design", 187 | "status": "PENDING_APPROVAL" 188 | } 189 | }, 190 | "XX204": { 191 | "attr": { 192 | "department": "marketing", 193 | "geography": "GB", 194 | "id": "XX204", 195 | "owner": "john", 196 | "team": "design", 197 | "status": "PENDING_APPROVAL" 198 | } 199 | }, 200 | "XX205": { 201 | "attr": { 202 | "department": "marketing", 203 | "geography": "GB", 204 | "id": "XX205", 205 | "owner": "john", 206 | "team": "design", 207 | "status": "PENDING_APPROVAL" 208 | } 209 | } 210 | } 211 | } 212 | } 213 | wantResponse: { 214 | "meta": null, 215 | "requestId": "test", 216 | "resourceInstances": { 217 | "XX100": { 218 | "actions": { 219 | "approve": "EFFECT_ALLOW", 220 | "view:public": "EFFECT_ALLOW", 221 | } 222 | }, 223 | "XX101": { 224 | "actions": { 225 | "approve": "EFFECT_ALLOW", 226 | "view:public": "EFFECT_ALLOW", 227 | } 228 | }, 229 | "XX102": { 230 | "actions": { 231 | "approve": "EFFECT_ALLOW", 232 | "view:public": "EFFECT_ALLOW", 233 | } 234 | }, 235 | "XX103": { 236 | "actions": { 237 | "approve": "EFFECT_ALLOW", 238 | "view:public": "EFFECT_ALLOW", 239 | } 240 | }, 241 | "XX104": { 242 | "actions": { 243 | "approve": "EFFECT_ALLOW", 244 | "view:public": "EFFECT_ALLOW", 245 | } 246 | }, 247 | "XX105": { 248 | "actions": { 249 | "approve": "EFFECT_ALLOW", 250 | "view:public": "EFFECT_ALLOW", 251 | } 252 | }, 253 | "XX106": { 254 | "actions": { 255 | "approve": "EFFECT_ALLOW", 256 | "view:public": "EFFECT_ALLOW", 257 | } 258 | }, 259 | "XX107": { 260 | "actions": { 261 | "approve": "EFFECT_ALLOW", 262 | "view:public": "EFFECT_ALLOW", 263 | } 264 | }, 265 | "XX108": { 266 | "actions": { 267 | "approve": "EFFECT_ALLOW", 268 | "view:public": "EFFECT_ALLOW", 269 | } 270 | }, 271 | "XX109": { 272 | "actions": { 273 | "approve": "EFFECT_ALLOW", 274 | "view:public": "EFFECT_ALLOW", 275 | } 276 | }, 277 | "XX110": { 278 | "actions": { 279 | "approve": "EFFECT_ALLOW", 280 | "view:public": "EFFECT_ALLOW", 281 | } 282 | }, 283 | "XX200": { 284 | "actions": { 285 | "approve": "EFFECT_DENY", 286 | "view:public": "EFFECT_ALLOW", 287 | } 288 | }, 289 | "XX201": { 290 | "actions": { 291 | "approve": "EFFECT_DENY", 292 | "view:public": "EFFECT_ALLOW", 293 | } 294 | }, 295 | "XX202": { 296 | "actions": { 297 | "approve": "EFFECT_DENY", 298 | "view:public": "EFFECT_ALLOW", 299 | } 300 | }, 301 | "XX203": { 302 | "actions": { 303 | "approve": "EFFECT_DENY", 304 | "view:public": "EFFECT_ALLOW", 305 | } 306 | }, 307 | "XX204": { 308 | "actions": { 309 | "approve": "EFFECT_DENY", 310 | "view:public": "EFFECT_ALLOW", 311 | } 312 | }, 313 | "XX205": { 314 | "actions": { 315 | "approve": "EFFECT_DENY", 316 | "view:public": "EFFECT_ALLOW", 317 | } 318 | } 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_case_08.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Aux Data" 3 | wantStatus: 4 | httpStatusCode: 200 5 | checkResourceSet: 6 | input: { 7 | "requestId": "test", 8 | "actions": [ 9 | "defer" 10 | ], 11 | "principal": { 12 | "id": "john", 13 | "policyVersion": "20210210", 14 | "roles": [ 15 | "employee" 16 | ], 17 | "attr": { 18 | "department": "marketing", 19 | "geography": "GB", 20 | "team": "design" 21 | } 22 | }, 23 | "resource": { 24 | "kind": "leave_request", 25 | "policyVersion": "20210210", 26 | "instances": { 27 | "XX125": { 28 | "attr": { 29 | "department": "marketing", 30 | "geography": "GB", 31 | "id": "XX125", 32 | "owner": "john", 33 | "team": "design" 34 | } 35 | } 36 | } 37 | }, 38 | "auxData": { 39 | "jwt": { 40 | "token": "eyJhbGciOiJFUzM4NCIsImtpZCI6IjE5TGZaYXRFZGc4M1lOYzVyMjNndU1KcXJuND0iLCJ0eXAiOiJKV1QifQ.eyJhdWQiOlsiY2VyYm9zLWp3dC10ZXN0cyJdLCJjdXN0b21BcnJheSI6WyJBIiwiQiIsIkMiXSwiY3VzdG9tSW50Ijo0MiwiY3VzdG9tTWFwIjp7IkEiOiJBQSIsIkIiOiJCQiIsIkMiOiJDQyJ9LCJjdXN0b21TdHJpbmciOiJmb29iYXIiLCJleHAiOjE5NDk5MzQwMzksImlzcyI6ImNlcmJvcy10ZXN0LXN1aXRlIn0.WN_tOScSpd_EI-P5EI1YlagxEgExSfBjAtcrgcF6lyWj1lGpR_GKx9goZEp2p_t5AVWXN_bjz_sMUmJdJa4cVd55Qm1miR-FKu6oNRHnSEWdMFmnArwPw-YDJWfylLFX" 41 | } 42 | } 43 | } 44 | wantResponse: { 45 | "meta": null, 46 | "requestId": "test", 47 | "resourceInstances": { 48 | "XX125": { 49 | "actions": { 50 | "defer": "EFFECT_ALLOW", 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_invalid_case_00.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Empty instances" 3 | wantStatus: 4 | httpStatusCode: 400 5 | wantError: true 6 | checkResourceSet: 7 | input: { 8 | "requestId": "test", 9 | "actions": [ 10 | "view:public", 11 | "approve" 12 | ], 13 | "principal": { 14 | "id": "john", 15 | "policyVersion": "20210210", 16 | "roles": [ 17 | "employee" 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "team": "design" 23 | } 24 | }, 25 | "resource": { 26 | "kind": "leave_request", 27 | "policyVersion": "20210210", 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/testdata/checks/check_resource_set/crs_invalid_case_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Missing resource kind" 3 | wantStatus: 4 | httpStatusCode: 400 5 | wantError: true 6 | checkResourceSet: 7 | input: { 8 | "requestId": "test", 9 | "actions": [ 10 | "view:public", 11 | "approve" 12 | ], 13 | "principal": { 14 | "id": "john", 15 | "policyVersion": "20210210", 16 | "roles": [ 17 | "employee" 18 | ], 19 | "attr": { 20 | "department": "marketing", 21 | "geography": "GB", 22 | "team": "design" 23 | } 24 | }, 25 | "resource": { 26 | "policyVersion": "20210210", 27 | "instances": { 28 | "XX125": { 29 | "attr": { 30 | "department": "marketing", 31 | "geography": "GB", 32 | "id": "XX125", 33 | "owner": "john", 34 | "team": "design" 35 | } 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/testdata/conf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | storage: 3 | driver: "disk" 4 | disk: 5 | directory: store 6 | 7 | auxData: 8 | jwt: 9 | keySets: 10 | - id: cerbos 11 | local: # Load from a local file 12 | file: verify_key.jwk -------------------------------------------------------------------------------- /test/testdata/store/derived_roles/common_roles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | description: |- 4 | Common dynamic roles used within the Apatr app 5 | derivedRoles: 6 | name: apatr_common_roles 7 | definitions: 8 | - name: owner 9 | parentRoles: ["user"] 10 | condition: 11 | match: 12 | expr: request.resource.attr.owner == request.principal.id 13 | 14 | - name: abuse_moderator 15 | parentRoles: ["moderator"] 16 | condition: 17 | match: 18 | expr: request.resource.attr.flagged == true 19 | -------------------------------------------------------------------------------- /test/testdata/store/derived_roles/derived_roles_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | derivedRoles: 4 | name: alpha 5 | definitions: 6 | - name: admin 7 | parentRoles: ["admin"] 8 | 9 | - name: tester 10 | parentRoles: ["dev", "qa"] 11 | 12 | - name: employee_that_owns_the_record 13 | parentRoles: ["employee"] 14 | condition: 15 | match: 16 | expr: R.attr.owner == P.id 17 | -------------------------------------------------------------------------------- /test/testdata/store/derived_roles/derived_roles_02.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | variables: 4 | same_geography: request.resource.attr.geography == request.principal.attr.geography 5 | derivedRoles: 6 | name: beta 7 | definitions: 8 | - name: any_employee 9 | parentRoles: ["employee"] 10 | 11 | - name: direct_manager 12 | parentRoles: ["manager"] 13 | condition: 14 | match: 15 | all: 16 | of: 17 | - expr: V.same_geography 18 | - expr: request.resource.attr.geography == request.principal.attr.managed_geographies 19 | -------------------------------------------------------------------------------- /test/testdata/store/derived_roles/derived_roles_03.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | derivedRoles: 4 | name: buyer_derived_roles 5 | definitions: 6 | - name: buyer 7 | parentRoles: ["user"] 8 | condition: 9 | match: 10 | all: 11 | of: 12 | - expr: R.attr.ownerOrgId == P.attr.orgId 13 | - expr: ("buyer" in P.attr.jobRoles) 14 | - expr: (R.attr.tags.brand in P.attr.tags.brands) || ("*" in P.attr.tags.brands) 15 | - expr: (R.attr.tags.class in P.attr.tags.classes) || ("*" in P.attr.tags.classes) 16 | - expr: (R.attr.tags.region in P.attr.tags.regions) || ("*" in P.attr.tags.regions) 17 | -------------------------------------------------------------------------------- /test/testdata/store/principal_policies/policy_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | variables: 4 | is_dev_record: request.resource.attr.dev_record == true 5 | principalPolicy: 6 | principal: donald_duck 7 | version: "20210210" 8 | rules: 9 | - resource: leave_request 10 | actions: 11 | - action: "*" 12 | condition: 13 | match: 14 | expr: variables.is_dev_record 15 | effect: EFFECT_ALLOW 16 | name: dev_admin 17 | 18 | - resource: salary_record 19 | actions: 20 | - action: "*" 21 | effect: EFFECT_DENY 22 | -------------------------------------------------------------------------------- /test/testdata/store/resource_policies/policy_01.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: api.cerbos.dev/v1 3 | variables: 4 | pending_approval: ("PENDING_APPROVAL") 5 | principal_location: |- 6 | (P.attr.ip_address.inIPAddrRange("10.20.0.0/16") ? "GB" : "") 7 | resourcePolicy: 8 | version: "20210210" 9 | importDerivedRoles: 10 | - alpha 11 | - beta 12 | resource: leave_request 13 | rules: 14 | - actions: ['*'] 15 | effect: EFFECT_ALLOW 16 | roles: 17 | - admin 18 | name: wildcard 19 | 20 | - actions: ["create"] 21 | derivedRoles: 22 | - employee_that_owns_the_record 23 | effect: EFFECT_ALLOW 24 | 25 | - actions: ["view:*"] 26 | derivedRoles: 27 | - employee_that_owns_the_record 28 | - direct_manager 29 | effect: EFFECT_ALLOW 30 | 31 | - actions: ["view:public"] 32 | derivedRoles: 33 | - any_employee 34 | effect: EFFECT_ALLOW 35 | name: public-view 36 | 37 | - actions: ["approve"] 38 | condition: 39 | match: 40 | expr: request.resource.attr.status == V.pending_approval 41 | derivedRoles: 42 | - direct_manager 43 | effect: EFFECT_ALLOW 44 | 45 | - actions: ["delete"] 46 | condition: 47 | match: 48 | expr: request.resource.attr.geography == variables.principal_location 49 | derivedRoles: 50 | - direct_manager 51 | effect: EFFECT_ALLOW 52 | 53 | - actions: ["defer"] 54 | effect: EFFECT_ALLOW 55 | roles: ["employee"] 56 | condition: 57 | match: 58 | all: 59 | of: 60 | - expr: '"cerbos-jwt-tests" in request.aux_data.jwt.aud' 61 | - expr: '"A" in request.aux_data.jwt.customArray' 62 | -------------------------------------------------------------------------------- /test/testdata/store/resource_policies/policy_02.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: api.cerbos.dev/v1 3 | resourcePolicy: 4 | resource: leave_request 5 | version: "staging" 6 | rules: 7 | - actions: ['*'] 8 | effect: EFFECT_ALLOW 9 | roles: 10 | - admin 11 | - actions: ["view:*"] 12 | roles: 13 | - employee 14 | effect: EFFECT_ALLOW 15 | - actions: ["view:public"] 16 | roles: 17 | - employee 18 | effect: EFFECT_ALLOW 19 | - actions: ["approve"] 20 | condition: 21 | match: 22 | expr: request.resource.attr.status == "PENDING_APPROVAL" 23 | roles: 24 | - manager 25 | effect: EFFECT_ALLOW 26 | -------------------------------------------------------------------------------- /test/testdata/store/resource_policies/policy_03.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: "api.cerbos.dev/v1" 3 | resourcePolicy: 4 | version: "default" 5 | importDerivedRoles: 6 | - buyer_derived_roles 7 | resource: purchase_order 8 | rules: 9 | - actions: ["*"] 10 | effect: EFFECT_ALLOW 11 | roles: 12 | - support 13 | - admin 14 | 15 | - actions: 16 | - create 17 | - view 18 | - update 19 | - delete 20 | effect: EFFECT_ALLOW 21 | derivedRoles: 22 | - buyer 23 | -------------------------------------------------------------------------------- /test/testdata/store/resource_policies/policy_04.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: api.cerbos.dev/v1 3 | resourcePolicy: 4 | version: "default" 5 | importDerivedRoles: 6 | - apatr_common_roles 7 | resource: "album:object" 8 | rules: 9 | - actions: ['*'] 10 | effect: EFFECT_ALLOW 11 | derivedRoles: 12 | - owner 13 | 14 | - actions: ['view', 'flag'] 15 | effect: EFFECT_ALLOW 16 | roles: 17 | - user 18 | condition: 19 | match: 20 | expr: request.resource.attr.public == true 21 | 22 | - actions: ['view', 'delete'] 23 | effect: EFFECT_ALLOW 24 | derivedRoles: 25 | - abuse_moderator 26 | -------------------------------------------------------------------------------- /test/testdata/verify_key.jwk: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-384", 5 | "kid": "19LfZatEdg83YNc5r23guMJqrn4=", 6 | "x": "NZa8GIgLBQC0DsmbISVHOhgkH41v3XxdNqRaOk1Pus3Mz3f46Lt6AP0ggqHPfbum", 7 | "y": "w-pnkb-ffeqd3tXihH33MNwseCYH7G3eN5nvCgdo-VY3xBhNJaSBQluJT4ggza9-", 8 | "alg": "ES384" 9 | } 10 | --------------------------------------------------------------------------------