├── .bin ├── go-licenses ├── license-engine.sh ├── license-template-go.tpl ├── licenses └── list-licenses ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── BUG-REPORT.yml │ ├── DESIGN-DOC.yml │ ├── FEATURE-REQUEST.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── auto_assign.yml ├── config.yml ├── pull_request_template.md └── workflows │ ├── closed_references.yml │ ├── conventional_commits.yml │ ├── go.yml │ ├── labels.yml │ ├── licenses.yml │ └── stale.yml ├── .gitignore ├── .reference-ignore ├── .reports └── dep-licenses.csv ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── HISTORY.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── MAINTAINERS ├── README.md ├── SECURITY.md ├── audit_logger.go ├── audit_logger_info.go ├── audit_logger_noop.go ├── audit_logger_test.go ├── benchmark_warden_test.go ├── compiler ├── README.md ├── regex.go └── regex_test.go ├── condition.go ├── condition_boolean.go ├── condition_cidr.go ├── condition_cidr_test.go ├── condition_resource_contains.go ├── condition_resource_contains_test.go ├── condition_string_equal.go ├── condition_string_match.go ├── condition_string_match_test.go ├── condition_string_pairs_equal.go ├── condition_string_pairs_equal_test.go ├── condition_subject_equal.go ├── condition_test.go ├── const.go ├── context.go ├── docs └── images │ └── banner_ladon.png ├── errors.go ├── errors_test.go ├── go.mod ├── go.sum ├── ladon.go ├── ladon_test.go ├── logo.png ├── manager.go ├── manager └── memory │ └── manager_memory.go ├── manager_all_test.go ├── manager_helper_test.go ├── manager_migrator.go ├── manager_mock_test.go ├── matcher.go ├── matcher_regexp.go ├── metric.go ├── metric_noop.go ├── policy.go ├── policy_test.go ├── warden.go └── warden_test.go /.bin/go-licenses: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ory/ladon/1d16bb356d68220899c40d8b4a81120af55a6482/.bin/go-licenses -------------------------------------------------------------------------------- /.bin/license-engine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script detects non-compliant licenses in the output of language-specific license checkers. 4 | 5 | # These licenses are allowed. 6 | # These are the exact and complete license strings for 100% legal certainty, no regexes. 7 | ALLOWED_LICENSES=( 8 | '0BSD' 9 | 'AFLv2.1' 10 | 'AFLv2.1,BSD' 11 | '(AFL-2.1 OR BSD-3-Clause)' 12 | 'Apache 2.0' 13 | 'Apache-2.0' 14 | '(Apache-2.0 OR MPL-1.1)' 15 | 'Apache-2.0 AND MIT' 16 | 'Apache License, Version 2.0' 17 | 'Apache*' 18 | 'Artistic-2.0' 19 | 'BlueOak-1.0.0' 20 | 'BSD' 21 | 'BSD*' 22 | 'BSD-2-Clause' 23 | '(BSD-2-Clause OR MIT OR Apache-2.0)' 24 | 'BSD-3-Clause' 25 | '(BSD-3-Clause OR GPL-2.0)' 26 | 'BSD-3-Clause OR MIT' 27 | '(BSD-3-Clause AND Apache-2.0)' 28 | 'CC0-1.0' 29 | 'CC-BY-3.0' 30 | 'CC-BY-4.0' 31 | '(CC-BY-4.0 AND MIT)' 32 | 'ISC' 33 | 'ISC*' 34 | 'LGPL-2.1' # LGPL allows commercial use, requires only that modifications to LGPL-protected libraries are published under a GPL-compatible license 35 | 'MIT' 36 | 'MIT*' 37 | 'MIT-0' 38 | 'MIT AND ISC' 39 | '(MIT AND BSD-3-Clause)' 40 | '(MIT AND Zlib)' 41 | '(MIT OR Apache-2.0)' 42 | '(MIT OR CC0-1.0)' 43 | '(MIT OR GPL-2.0)' 44 | 'MPL-2.0' 45 | '(MPL-2.0 OR Apache-2.0)' 46 | 'Public Domain' 47 | 'Python-2.0' # the Python-2.0 is a permissive license, see https://en.wikipedia.org/wiki/Python_License 48 | 'Unlicense' 49 | 'WTFPL' 50 | 'WTFPL OR ISC' 51 | '(WTFPL OR MIT)' 52 | '(MIT OR WTFPL)' 53 | 'LGPL-3.0-or-later' # Requires only that modifications to LGPL-protected libraries are published under a GPL-compatible license which is not the case at Ory 54 | ) 55 | 56 | # These modules don't work with the current license checkers 57 | # and have been manually verified to have a compatible license (regex format). 58 | APPROVED_MODULES=( 59 | 'https://github.com/ory-corp/cloud/' # Ory IP 60 | 'github.com/ory/hydra-client-go' # Apache-2.0 61 | 'github.com/ory/hydra-client-go/v2' # Apache-2.0 62 | 'github.com/ory/kratos-client-go' # Apache-2.0 63 | 'github.com/gobuffalo/github_flavored_markdown' # MIT 64 | 'buffers@0.1.1' # MIT: original source at http://github.com/substack/node-bufferlist is deleted but a fork at https://github.com/pkrumins/node-bufferlist/blob/master/LICENSE contains the original license by the original author (James Halliday) 65 | 'https://github.com/iconify/iconify/packages/react' # MIT: license is in root of monorepo at https://github.com/iconify/iconify/blob/main/license.txt 66 | 'github.com/gobuffalo/.*' # MIT: license is in root of monorepo at https://github.com/gobuffalo/github_flavored_markdown/blob/main/LICENSE 67 | 'github.com/ory-corp/cloud/.*' # Ory IP 68 | 'github.com/golang/freetype/.*' # FreeType license: https://freetype.sourceforge.net/FTL.TXT 69 | 'go.opentelemetry.io/otel/exporters/jaeger/internal/third_party/thrift/lib/go/thrift' # Incorrect detection, actually Apache-2.0: https://github.com/open-telemetry/opentelemetry-go/blob/exporters/jaeger/v1.17.0/exporters/jaeger/internal/third_party/thrift/LICENSE 70 | 'go.uber.org/zap/exp/.*' # MIT license is in root of exp folder in monorepo at https://github.com/uber-go/zap/blob/master/exp/LICENSE 71 | 'github.com/ory/client-go' # Apache-2.0 72 | 'github.com/ian-kent/linkio' # BSD - https://github.com/ian-kent/linkio/blob/97566b8728870dac1c9863ba5b0f237c39166879/linkio.go#L1-L3 73 | 'github.com/t-k/fluent-logger-golang/fluent' # Apache-2.0 https://github.com/t-k/fluent-logger-golang/blob/master/LICENSE 74 | 'github.com/jmespath/go-jmespath' # Apache-2.0 https://github.com/jmespath/go-jmespath/blob/master/LICENSE 75 | 'github.com/ory/keto/proto/ory/keto/opl/v1alpha1' # Apache-2.0 - submodule of keto 76 | 'github.com/ory/keto/proto/ory/keto/relation_tuples/v1alpha2' # Apache-2.0 - submodule of keto 77 | '@ory-corp/.*' # Ory IP 78 | 'github.com/apache/arrow/.*' # Apache-2.0 https://github.com/apache/arrow/blob/main/LICENSE.txt 79 | 'github.com/ory-corp/webhook-target' # Ory IP 80 | '@ory/keto-grpc-client.*' # Apache-2.0 - submodule of keto 81 | 'golden-fleece@1.0.9' # MIT: https://github.com/Rich-Harris/golden-fleece/blob/master/LICENSE 82 | 'github.com/gogo/googleapis/.*' # Apache-2.0 https://github.com/gogo/googleapis/blob/master/LICENSE 83 | ) 84 | 85 | # These lines in the output should be ignored (plain text, no regex). 86 | IGNORE_LINES=( 87 | '"module name","licenses"' # header of license output for Node.js 88 | ) 89 | 90 | echo_green() { 91 | printf "\e[1;92m%s\e[0m\n" "$@" 92 | } 93 | 94 | echo_red() { 95 | printf "\e[0;91m%s\e[0m\n" "$@" 96 | } 97 | 98 | # capture STDIN 99 | input=$(cat -) 100 | 101 | # remove ignored lines 102 | for ignored in "${IGNORE_LINES[@]}"; do 103 | input=$(echo "$input" | grep -vF "$ignored") 104 | done 105 | 106 | # remove pre-approved modules 107 | for approved in "${APPROVED_MODULES[@]}"; do 108 | input=$(echo "$input" | grep -vE "\"${approved}\"") 109 | input=$(echo "$input" | grep -vE "\"Custom: ${approved}\"") 110 | done 111 | 112 | # remove allowed licenses 113 | for allowed in "${ALLOWED_LICENSES[@]}"; do 114 | input=$(echo "$input" | grep -vF "\"${allowed}\"") 115 | done 116 | 117 | # anything left in the input at this point is a module with an invalid license 118 | 119 | # print outcome 120 | if [ -z "$input" ]; then 121 | echo_green "Licenses are okay." 122 | else 123 | echo_red "Unknown licenses found!" 124 | echo "$input" 125 | exit 1 126 | fi 127 | -------------------------------------------------------------------------------- /.bin/license-template-go.tpl: -------------------------------------------------------------------------------- 1 | {{ range . }} 2 | "{{.Name}}","{{.LicenseName}}" 3 | {{- end }} 4 | -------------------------------------------------------------------------------- /.bin/licenses: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # Get the directory where this script is located 5 | bin_dir="$(cd "$(dirname "$0")" && pwd)" 6 | 7 | { echo "Checking licenses ..."; } 2>/dev/null 8 | "${bin_dir}/list-licenses" | "${bin_dir}/license-engine.sh" 9 | -------------------------------------------------------------------------------- /.bin/list-licenses: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | bin_dir="$(cd "$(dirname "$0")" && pwd)" 5 | 6 | # list Node licenses 7 | if [ -f package.json ]; then 8 | if jq -e '.dependencies and (.dependencies | keys | length > 0)' package.json >/dev/null; then 9 | npx --yes license-checker --production --csv --excludePrivatePackages --customPath "${bin_dir}"/license-template-node.json | grep -v '^$' 10 | echo 11 | else 12 | echo "No dependencies found in package.json" >&2 13 | echo 14 | fi 15 | fi 16 | 17 | # list Go licenses 18 | if [ -f go.mod ]; then 19 | # List all direct Go module dependencies, transform their paths to root module paths 20 | # (e.g., github.com/ory/x instead of github.com/ory/x/foo/bar), and generate a license report 21 | # for each unique root module. This ensures that the license report is generated for the root 22 | # module of a repository, where licenses are typically defined. 23 | go_modules=$( 24 | go list -f "{{if not .Indirect}}{{.Path}}{{end}}" -m ... | 25 | sort -u | 26 | awk -F/ '{ if ($1 == "github.com" && NF >= 3) { print $1"/"$2"/"$3 } else { print } }' | 27 | sort -u 28 | ) 29 | if [ -z "$go_modules" ]; then 30 | echo "No Go modules found" >&2 31 | else 32 | # Workaround until https://github.com/google/go-licenses/issues/307 is fixed 33 | # .bin/go-licenses report "$module_name" --template .bin/license-template-go.tpl 2>/dev/null 34 | # 35 | echo "$go_modules" | xargs -I {} sh -c '.bin/go-licenses report --template .bin/license-template-go.tpl {}' | grep -v '^$' 36 | echo 37 | fi 38 | fi 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | ; Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | charset = utf-8 12 | 13 | ; Golang 14 | [*.go] 15 | indent_style = tab 16 | indent_size = 4 17 | 18 | ; YAML 19 | [*.{yaml,yml}] 20 | indent_size = 2 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/FUNDING.yml 3 | 4 | # These are supported funding model platforms 5 | 6 | # github: 7 | patreon: _ory 8 | open_collective: ory 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 14 | 15 | **Do you want to request a *feature* or report a *bug*?** 16 | 17 | **What is the current behavior?** 18 | 19 | **If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem.** 20 | 21 | **What is the expected behavior?** 22 | 23 | **Which version of the software is affected?** 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG-REPORT.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/BUG-REPORT.yml 3 | 4 | description: "Create a bug report" 5 | labels: 6 | - bug 7 | name: "Bug Report" 8 | body: 9 | - attributes: 10 | value: "Thank you for taking the time to fill out this bug report!\n" 11 | type: markdown 12 | - attributes: 13 | label: "Preflight checklist" 14 | options: 15 | - label: 16 | "I could not find a solution in the existing issues, docs, nor 17 | discussions." 18 | required: true 19 | - label: 20 | "I agree to follow this project's [Code of 21 | Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." 22 | required: true 23 | - label: 24 | "I have read and am following this repository's [Contribution 25 | Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." 26 | required: true 27 | - label: 28 | "I have joined the [Ory Community Slack](https://slack.ory.sh)." 29 | - label: 30 | "I am signed up to the [Ory Security Patch 31 | Newsletter](https://www.ory.sh/l/sign-up-newsletter)." 32 | id: checklist 33 | type: checkboxes 34 | - attributes: 35 | description: 36 | "Enter the slug or API URL of the affected Ory Network project. Leave 37 | empty when you are self-hosting." 38 | label: "Ory Network Project" 39 | placeholder: "https://.projects.oryapis.com" 40 | id: ory-network-project 41 | type: input 42 | - attributes: 43 | description: "A clear and concise description of what the bug is." 44 | label: "Describe the bug" 45 | placeholder: "Tell us what you see!" 46 | id: describe-bug 47 | type: textarea 48 | validations: 49 | required: true 50 | - attributes: 51 | description: | 52 | Clear, formatted, and easy to follow steps to reproduce the behavior: 53 | placeholder: | 54 | Steps to reproduce the behavior: 55 | 56 | 1. Run `docker run ....` 57 | 2. Make API Request to with `curl ...` 58 | 3. Request fails with response: `{"some": "error"}` 59 | label: "Reproducing the bug" 60 | id: reproduce-bug 61 | type: textarea 62 | validations: 63 | required: true 64 | - attributes: 65 | description: 66 | "Please copy and paste any relevant log output. This will be 67 | automatically formatted into code, so no need for backticks. Please 68 | redact any sensitive information" 69 | label: "Relevant log output" 70 | render: shell 71 | placeholder: | 72 | log=error .... 73 | id: logs 74 | type: textarea 75 | - attributes: 76 | description: 77 | "Please copy and paste any relevant configuration. This will be 78 | automatically formatted into code, so no need for backticks. Please 79 | redact any sensitive information!" 80 | label: "Relevant configuration" 81 | render: yml 82 | placeholder: | 83 | server: 84 | admin: 85 | port: 1234 86 | id: config 87 | type: textarea 88 | - attributes: 89 | description: "What version of our software are you running?" 90 | label: Version 91 | id: version 92 | type: input 93 | validations: 94 | required: true 95 | - attributes: 96 | label: "On which operating system are you observing this issue?" 97 | options: 98 | - Ory Network 99 | - macOS 100 | - Linux 101 | - Windows 102 | - FreeBSD 103 | - Other 104 | id: operating-system 105 | type: dropdown 106 | - attributes: 107 | label: "In which environment are you deploying?" 108 | options: 109 | - Ory Network 110 | - Docker 111 | - "Docker Compose" 112 | - "Kubernetes with Helm" 113 | - Kubernetes 114 | - Binary 115 | - Other 116 | id: deployment 117 | type: dropdown 118 | - attributes: 119 | description: "Add any other context about the problem here." 120 | label: Additional Context 121 | id: additional 122 | type: textarea 123 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DESIGN-DOC.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml 3 | 4 | description: 5 | "A design document is needed for non-trivial changes to the code base." 6 | labels: 7 | - rfc 8 | name: "Design Document" 9 | body: 10 | - attributes: 11 | value: | 12 | Thank you for writing this design document. 13 | 14 | One of the key elements of Ory's software engineering culture is the use of defining software designs through design docs. These are relatively informal documents that the primary author or authors of a software system or application create before they embark on the coding project. The design doc documents the high level implementation strategy and key design decisions with emphasis on the trade-offs that were considered during those decisions. 15 | 16 | Ory is leaning heavily on [Google's design docs process](https://www.industrialempathy.com/posts/design-docs-at-google/) 17 | and [Golang Proposals](https://github.com/golang/proposal). 18 | 19 | Writing a design doc before contributing your change ensures that your ideas are checked with 20 | the community and maintainers. It will save you a lot of time developing things that might need to be changed 21 | after code reviews, and your pull requests will be merged faster. 22 | type: markdown 23 | - attributes: 24 | label: "Preflight checklist" 25 | options: 26 | - label: 27 | "I could not find a solution in the existing issues, docs, nor 28 | discussions." 29 | required: true 30 | - label: 31 | "I agree to follow this project's [Code of 32 | Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." 33 | required: true 34 | - label: 35 | "I have read and am following this repository's [Contribution 36 | Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." 37 | required: true 38 | - label: 39 | "I have joined the [Ory Community Slack](https://slack.ory.sh)." 40 | - label: 41 | "I am signed up to the [Ory Security Patch 42 | Newsletter](https://www.ory.sh/l/sign-up-newsletter)." 43 | id: checklist 44 | type: checkboxes 45 | - attributes: 46 | description: 47 | "Enter the slug or API URL of the affected Ory Network project. Leave 48 | empty when you are self-hosting." 49 | label: "Ory Network Project" 50 | placeholder: "https://.projects.oryapis.com" 51 | id: ory-network-project 52 | type: input 53 | - attributes: 54 | description: | 55 | This section gives the reader a very rough overview of the landscape in which the new system is being built and what is actually being built. This isn’t a requirements doc. Keep it succinct! The goal is that readers are brought up to speed but some previous knowledge can be assumed and detailed info can be linked to. This section should be entirely focused on objective background facts. 56 | label: "Context and scope" 57 | id: scope 58 | type: textarea 59 | validations: 60 | required: true 61 | 62 | - attributes: 63 | description: | 64 | A short list of bullet points of what the goals of the system are, and, sometimes more importantly, what non-goals are. Note, that non-goals aren’t negated goals like “The system shouldn’t crash”, but rather things that could reasonably be goals, but are explicitly chosen not to be goals. A good example would be “ACID compliance”; when designing a database, you’d certainly want to know whether that is a goal or non-goal. And if it is a non-goal you might still select a solution that provides it, if it doesn’t introduce trade-offs that prevent achieving the goals. 65 | label: "Goals and non-goals" 66 | id: goals 67 | type: textarea 68 | validations: 69 | required: true 70 | 71 | - attributes: 72 | description: | 73 | This section should start with an overview and then go into details. 74 | The design doc is the place to write down the trade-offs you made in designing your software. Focus on those trade-offs to produce a useful document with long-term value. That is, given the context (facts), goals and non-goals (requirements), the design doc is the place to suggest solutions and show why a particular solution best satisfies those goals. 75 | 76 | The point of writing a document over a more formal medium is to provide the flexibility to express the problem at hand in an appropriate manner. Because of this, there is no explicit guidance on how to actually describe the design. 77 | label: "The design" 78 | id: design 79 | type: textarea 80 | validations: 81 | required: true 82 | 83 | - attributes: 84 | description: | 85 | If the system under design exposes an API, then sketching out that API is usually a good idea. In most cases, however, one should withstand the temptation to copy-paste formal interface or data definitions into the doc as these are often verbose, contain unnecessary detail and quickly get out of date. Instead, focus on the parts that are relevant to the design and its trade-offs. 86 | label: "APIs" 87 | id: apis 88 | type: textarea 89 | 90 | - attributes: 91 | description: | 92 | Systems that store data should likely discuss how and in what rough form this happens. Similar to the advice on APIs, and for the same reasons, copy-pasting complete schema definitions should be avoided. Instead, focus on the parts that are relevant to the design and its trade-offs. 93 | label: "Data storage" 94 | id: persistence 95 | type: textarea 96 | 97 | - attributes: 98 | description: | 99 | Design docs should rarely contain code, or pseudo-code except in situations where novel algorithms are described. As appropriate, link to prototypes that show the feasibility of the design. 100 | label: "Code and pseudo-code" 101 | id: pseudocode 102 | type: textarea 103 | 104 | - attributes: 105 | description: | 106 | One of the primary factors that would influence the shape of a software design and hence the design doc, is the degree of constraint of the solution space. 107 | 108 | On one end of the extreme is the “greenfield software project”, where all we know are the goals, and the solution can be whatever makes the most sense. Such a document may be wide-ranging, but it also needs to quickly define a set of rules that allow zooming in on a manageable set of solutions. 109 | 110 | On the other end are systems where the possible solutions are very well defined, but it isn't at all obvious how they could even be combined to achieve the goals. This may be a legacy system that is difficult to change and wasn't designed to do what you want it to do or a library design that needs to operate within the constraints of the host programming language. 111 | 112 | In this situation, you may be able to enumerate all the things you can do relatively easily, but you need to creatively put those things together to achieve the goals. There may be multiple solutions, and none of them are great, and hence such a document should focus on selecting the best way given all identified trade-offs. 113 | label: "Degree of constraint" 114 | id: constrait 115 | type: textarea 116 | 117 | - attributes: 118 | description: | 119 | This section lists alternative designs that would have reasonably achieved similar outcomes. The focus should be on the trade-offs that each respective design makes and how those trade-offs led to the decision to select the design that is the primary topic of the document. 120 | 121 | While it is fine to be succinct about a solution that ended up not being selected, this section is one of the most important ones as it shows very explicitly why the selected solution is the best given the project goals and how other solutions, that the reader may be wondering about, introduce trade-offs that are less desirable given the goals. 122 | 123 | label: Alternatives considered 124 | id: alternatives 125 | type: textarea 126 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml 3 | 4 | description: 5 | "Suggest an idea for this project without a plan for implementation" 6 | labels: 7 | - feat 8 | name: "Feature Request" 9 | body: 10 | - attributes: 11 | value: | 12 | Thank you for suggesting an idea for this project! 13 | 14 | If you already have a plan to implement a feature or a change, please create a [design document](https://github.com/aeneasr/gh-template-test/issues/new?assignees=&labels=rfc&template=DESIGN-DOC.yml) instead if the change is non-trivial! 15 | type: markdown 16 | - attributes: 17 | label: "Preflight checklist" 18 | options: 19 | - label: 20 | "I could not find a solution in the existing issues, docs, nor 21 | discussions." 22 | required: true 23 | - label: 24 | "I agree to follow this project's [Code of 25 | Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." 26 | required: true 27 | - label: 28 | "I have read and am following this repository's [Contribution 29 | Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." 30 | required: true 31 | - label: 32 | "I have joined the [Ory Community Slack](https://slack.ory.sh)." 33 | - label: 34 | "I am signed up to the [Ory Security Patch 35 | Newsletter](https://www.ory.sh/l/sign-up-newsletter)." 36 | id: checklist 37 | type: checkboxes 38 | - attributes: 39 | description: 40 | "Enter the slug or API URL of the affected Ory Network project. Leave 41 | empty when you are self-hosting." 42 | label: "Ory Network Project" 43 | placeholder: "https://.projects.oryapis.com" 44 | id: ory-network-project 45 | type: input 46 | - attributes: 47 | description: 48 | "Is your feature request related to a problem? Please describe." 49 | label: "Describe your problem" 50 | placeholder: 51 | "A clear and concise description of what the problem is. Ex. I'm always 52 | frustrated when [...]" 53 | id: problem 54 | type: textarea 55 | validations: 56 | required: true 57 | - attributes: 58 | description: | 59 | Describe the solution you'd like 60 | placeholder: | 61 | A clear and concise description of what you want to happen. 62 | label: "Describe your ideal solution" 63 | id: solution 64 | type: textarea 65 | validations: 66 | required: true 67 | - attributes: 68 | description: "Describe alternatives you've considered" 69 | label: "Workarounds or alternatives" 70 | id: alternatives 71 | type: textarea 72 | validations: 73 | required: true 74 | - attributes: 75 | description: "What version of our software are you running?" 76 | label: Version 77 | id: version 78 | type: input 79 | validations: 80 | required: true 81 | - attributes: 82 | description: 83 | "Add any other context or screenshots about the feature request here." 84 | label: Additional Context 85 | id: additional 86 | type: textarea 87 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/config.yml 3 | 4 | blank_issues_enabled: false 5 | contact_links: 6 | - name: Ory ladon Forum 7 | url: https://github.com/orgs/ory/discussions 8 | about: 9 | Please ask and answer questions here, show your implementations and 10 | discuss ideas. 11 | - name: Ory Chat 12 | url: https://www.ory.sh/chat 13 | about: 14 | Hang out with other Ory community members to ask and answer questions. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/auto_assign.yml 3 | 4 | # Set to true to add reviewers to pull requests 5 | addReviewers: true 6 | 7 | # Set to true to add assignees to pull requests 8 | addAssignees: true 9 | 10 | # A list of reviewers to be added to pull requests (GitHub user name) 11 | assignees: 12 | - ory/maintainers 13 | 14 | # A number of reviewers added to the pull request 15 | # Set 0 to add all the reviewers (default: 0) 16 | numberOfReviewers: 0 17 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/config.yml 3 | 4 | todo: 5 | keyword: "@todo" 6 | label: todo 7 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 12 | 13 | ## Related Issue or Design Document 14 | 15 | 29 | 30 | ## Checklist 31 | 32 | 36 | 37 | - [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md) and signed the CLA. 38 | - [ ] I have referenced an issue containing the design document if my change introduces a new feature. 39 | - [ ] I have read the [security policy](../security/policy). 40 | - [ ] I confirm that this pull request does not address a security vulnerability. 41 | If this pull request addresses a security vulnerability, 42 | I confirm that I got approval (please contact [security@ory.sh](mailto:security@ory.sh)) from the maintainers to push the changes. 43 | - [ ] I have added tests that prove my fix is effective or that my feature works. 44 | - [ ] I have added the necessary documentation within the code base (if appropriate). 45 | 46 | ## Further comments 47 | 48 | 52 | -------------------------------------------------------------------------------- /.github/workflows/closed_references.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/closed_references.yml 3 | 4 | name: Closed Reference Notifier 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | workflow_dispatch: 10 | inputs: 11 | issueLimit: 12 | description: Max. number of issues to create 13 | required: true 14 | default: "5" 15 | 16 | jobs: 17 | find_closed_references: 18 | if: github.repository_owner == 'ory' 19 | runs-on: ubuntu-latest 20 | name: Find closed references 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: actions/setup-node@v2-beta 24 | with: 25 | node-version: "14" 26 | - uses: ory/closed-reference-notifier@v1 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | issueLabels: upstream,good first issue,help wanted 30 | issueLimit: ${{ github.event.inputs.issueLimit || '5' }} 31 | -------------------------------------------------------------------------------- /.github/workflows/conventional_commits.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/conventional_commits.yml 3 | 4 | name: Conventional commits 5 | 6 | # This GitHub CI Action enforces that pull request titles follow conventional commits. 7 | # More info at https://www.conventionalcommits.org. 8 | # 9 | # The Ory-wide defaults for commit titles and scopes are below. 10 | # Your repository can add/replace elements via a configuration file at the path below. 11 | # More info at https://github.com/ory/ci/blob/master/conventional_commit_config/README.md 12 | 13 | on: 14 | pull_request_target: 15 | types: 16 | - edited 17 | - opened 18 | - ready_for_review 19 | - reopened 20 | # pull_request: # for debugging, uses config in local branch but supports only Pull Requests from this repo 21 | 22 | jobs: 23 | main: 24 | name: Validate PR title 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v3 28 | - id: config 29 | uses: ory/ci/conventional_commit_config@master 30 | with: 31 | config_path: .github/conventional_commits.json 32 | default_types: | 33 | feat 34 | fix 35 | revert 36 | docs 37 | style 38 | refactor 39 | test 40 | build 41 | autogen 42 | security 43 | ci 44 | chore 45 | default_scopes: | 46 | deps 47 | docs 48 | default_require_scope: false 49 | - uses: amannn/action-semantic-pull-request@v4 50 | env: 51 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | with: 53 | types: ${{ steps.config.outputs.types }} 54 | scopes: ${{ steps.config.outputs.scopes }} 55 | requireScope: ${{ steps.config.outputs.requireScope }} 56 | subjectPattern: ^(?![A-Z]).+$ 57 | subjectPatternError: | 58 | The subject should start with a lowercase letter, yours is uppercase: 59 | "{subject}" 60 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Test 25 | run: go test -v ./... 26 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/labels.yml 3 | 4 | name: Synchronize Issue Labels 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | milestone: 14 | if: github.repository_owner == 'ory' 15 | name: Synchronize Issue Labels 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Synchronize Issue Labels 21 | uses: ory/label-sync-action@v0 22 | with: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | dry: false 25 | forced: true 26 | -------------------------------------------------------------------------------- /.github/workflows/licenses.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/licenses.yml 3 | 4 | name: Licenses 5 | 6 | on: 7 | pull_request: 8 | push: 9 | branches: 10 | - main 11 | - v3 12 | - master 13 | 14 | jobs: 15 | licenses: 16 | name: License compliance 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Install script 20 | uses: ory/ci/licenses/setup@master 21 | with: 22 | token: ${{ secrets.ORY_BOT_PAT || secrets.GITHUB_TOKEN }} 23 | - name: Check licenses 24 | uses: ory/ci/licenses/check@master 25 | - name: Write, commit, push licenses 26 | uses: ory/ci/licenses/write@master 27 | if: 28 | ${{ github.ref == 'refs/heads/main' || github.ref == 29 | 'refs/heads/master' || github.ref == 'refs/heads/v3' }} 30 | with: 31 | author-email: 32 | ${{ secrets.ORY_BOT_PAT && 33 | '60093411+ory-bot@users.noreply.github.com' || 34 | format('{0}@users.noreply.github.com', github.actor) }} 35 | author-name: ${{ secrets.ORY_BOT_PAT && 'ory-bot' || github.actor }} 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # AUTO-GENERATED, DO NOT EDIT! 2 | # Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/stale.yml 3 | 4 | name: "Close Stale Issues" 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 0 * * *" 9 | 10 | jobs: 11 | stale: 12 | if: github.repository_owner == 'ory' 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | stale-issue-message: | 19 | Hello contributors! 20 | 21 | I am marking this issue as stale as it has not received any engagement from the community or maintainers for a year. That does not imply that the issue has no merit! If you feel strongly about this issue 22 | 23 | - open a PR referencing and resolving the issue; 24 | - leave a comment on it and discuss ideas on how you could contribute towards resolving it; 25 | - leave a comment and describe in detail why this issue is critical for your use case; 26 | - open a new issue with updated details and a plan for resolving the issue. 27 | 28 | Throughout its lifetime, Ory has received over 10.000 issues and PRs. To sustain that growth, we need to prioritize and focus on issues that are important to the community. A good indication of importance, and thus priority, is activity on a topic. 29 | 30 | Unfortunately, [burnout](https://www.jeffgeerling.com/blog/2016/why-i-close-prs-oss-project-maintainer-notes) has become a [topic](https://opensource.guide/best-practices/#its-okay-to-hit-pause) of [concern](https://docs.brew.sh/Maintainers-Avoiding-Burnout) amongst open-source projects. 31 | 32 | It can lead to severe personal and health issues as well as [opening](https://haacked.com/archive/2019/05/28/maintainer-burnout/) catastrophic [attack vectors](https://www.gradiant.org/en/blog/open-source-maintainer-burnout-as-an-attack-surface/). 33 | 34 | The motivation for this automation is to help prioritize issues in the backlog and not ignore, reject, or belittle anyone. 35 | 36 | If this issue was marked as stale erroneously you can exempt it by adding the `backlog` label, assigning someone, or setting a milestone for it. 37 | 38 | Thank you for your understanding and to anyone who participated in the conversation! And as written above, please do participate in the conversation if this topic is important to you! 39 | 40 | Thank you 🙏✌️ 41 | stale-issue-label: "stale" 42 | exempt-issue-labels: "bug,blocking,docs,backlog" 43 | days-before-stale: 365 44 | days-before-close: 30 45 | exempt-milestones: true 46 | exempt-assignees: true 47 | only-pr-labels: "stale" 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | vendor/ 4 | sqlite-test.db 5 | tests/ 6 | -------------------------------------------------------------------------------- /.reference-ignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | docs 3 | CHANGELOG.md 4 | -------------------------------------------------------------------------------- /.reports/dep-licenses.csv: -------------------------------------------------------------------------------- 1 | "github.com/dlclark/regexp2","MIT" 2 | "github.com/hashicorp/golang-lru","MPL-2.0" 3 | "github.com/dlclark/regexp2","MIT" 4 | "github.com/hashicorp/golang-lru","MPL-2.0" 5 | "github.com/ory/ladon","Apache-2.0" 6 | "github.com/pkg/errors","BSD-2-Clause" 7 | "github.com/ory/pagination","Apache-2.0" 8 | "github.com/google/uuid","BSD-3-Clause" 9 | "github.com/pborman/uuid","BSD-3-Clause" 10 | "github.com/pkg/errors","BSD-2-Clause" 11 | 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | env: 4 | - GO111MODULE=on 5 | 6 | language: go 7 | 8 | go_import_path: github.com/ory/ladon 9 | 10 | go: 11 | - 1.11 12 | 13 | install: 14 | - go get github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/pierrre/gotestcover 15 | 16 | script: 17 | - gotestcover -coverprofile="cover.out" -race -covermode="atomic" $(go list ./... | grep -v /vendor/) 18 | 19 | after_success: 20 | - go vet -x $(go list ./... | grep -v /vendor/) 21 | - golint $(go list ./... | grep -v /vendor/) 22 | - goveralls -coverprofile="cover.out" 23 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Contributor Covenant Code of Conduct 5 | 6 | ## Our Pledge 7 | 8 | We as members, contributors, and leaders pledge to make participation in our 9 | community a harassment-free experience for everyone, regardless of age, body 10 | size, visible or invisible disability, ethnicity, sex characteristics, gender 11 | identity and expression, level of experience, education, socio-economic status, 12 | nationality, personal appearance, race, caste, color, religion, or sexual 13 | identity and orientation. 14 | 15 | We pledge to act and interact in ways that contribute to an open, welcoming, 16 | diverse, inclusive, and healthy community. 17 | 18 | ## Our Standards 19 | 20 | Examples of behavior that contributes to a positive environment for our 21 | community include: 22 | 23 | - Demonstrating empathy and kindness toward other people 24 | - Being respectful of differing opinions, viewpoints, and experiences 25 | - Giving and gracefully accepting constructive feedback 26 | - Accepting responsibility and apologizing to those affected by our mistakes, 27 | and learning from the experience 28 | - Focusing on what is best not just for us as individuals, but for the overall 29 | community 30 | 31 | Examples of unacceptable behavior include: 32 | 33 | - The use of sexualized language or imagery, and sexual attention or advances of 34 | any kind 35 | - Trolling, insulting or derogatory comments, and personal or political attacks 36 | - Public or private harassment 37 | - Publishing others' private information, such as a physical or email address, 38 | without their explicit permission 39 | - Other conduct which could reasonably be considered inappropriate in a 40 | professional setting 41 | 42 | ## Open Source Community Support 43 | 44 | Ory Open source software is collaborative and based on contributions by 45 | developers in the Ory community. There is no obligation from Ory to help with 46 | individual problems. If Ory open source software is used in production in a 47 | for-profit company or enterprise environment, we mandate a paid support contract 48 | where Ory is obligated under their service level agreements (SLAs) to offer a 49 | defined level of availability and responsibility. For more information about 50 | paid support please contact us at sales@ory.sh. 51 | 52 | ## Enforcement Responsibilities 53 | 54 | Community leaders are responsible for clarifying and enforcing our standards of 55 | acceptable behavior and will take appropriate and fair corrective action in 56 | response to any behavior that they deem inappropriate, threatening, offensive, 57 | or harmful. 58 | 59 | Community leaders have the right and responsibility to remove, edit, or reject 60 | comments, commits, code, wiki edits, issues, and other contributions that are 61 | not aligned to this Code of Conduct, and will communicate reasons for moderation 62 | decisions when appropriate. 63 | 64 | ## Scope 65 | 66 | This Code of Conduct applies within all community spaces, and also applies when 67 | an individual is officially representing the community in public spaces. 68 | Examples of representing our community include using an official e-mail address, 69 | posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. 71 | 72 | ## Enforcement 73 | 74 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 75 | reported to the community leaders responsible for enforcement at 76 | [office@ory.sh](mailto:office@ory.sh). All complaints will be reviewed and 77 | investigated promptly and fairly. 78 | 79 | All community leaders are obligated to respect the privacy and security of the 80 | reporter of any incident. 81 | 82 | ## Enforcement Guidelines 83 | 84 | Community leaders will follow these Community Impact Guidelines in determining 85 | the consequences for any action they deem in violation of this Code of Conduct: 86 | 87 | ### 1. Correction 88 | 89 | **Community Impact**: Use of inappropriate language or other behavior deemed 90 | unprofessional or unwelcome in the community. 91 | 92 | **Consequence**: A private, written warning from community leaders, providing 93 | clarity around the nature of the violation and an explanation of why the 94 | behavior was inappropriate. A public apology may be requested. 95 | 96 | ### 2. Warning 97 | 98 | **Community Impact**: A violation through a single incident or series of 99 | actions. 100 | 101 | **Consequence**: A warning with consequences for continued behavior. No 102 | interaction with the people involved, including unsolicited interaction with 103 | those enforcing the Code of Conduct, for a specified period of time. This 104 | includes avoiding interactions in community spaces as well as external channels 105 | like social media. Violating these terms may lead to a temporary or permanent 106 | ban. 107 | 108 | ### 3. Temporary Ban 109 | 110 | **Community Impact**: A serious violation of community standards, including 111 | sustained inappropriate behavior. 112 | 113 | **Consequence**: A temporary ban from any sort of interaction or public 114 | communication with the community for a specified period of time. No public or 115 | private interaction with the people involved, including unsolicited interaction 116 | with those enforcing the Code of Conduct, is allowed during this period. 117 | Violating these terms may lead to a permanent ban. 118 | 119 | ### 4. Permanent Ban 120 | 121 | **Community Impact**: Demonstrating a pattern of violation of community 122 | standards, including sustained inappropriate behavior, harassment of an 123 | individual, or aggression toward or disparagement of classes of individuals. 124 | 125 | **Consequence**: A permanent ban from any sort of public interaction within the 126 | community. 127 | 128 | ## Attribution 129 | 130 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 131 | version 2.1, available at 132 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 133 | 134 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 135 | enforcement ladder][mozilla coc]. 136 | 137 | For answers to common questions about this code of conduct, see the FAQ at 138 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 139 | [https://www.contributor-covenant.org/translations][translations]. 140 | 141 | [homepage]: https://www.contributor-covenant.org 142 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 143 | [mozilla coc]: https://github.com/mozilla/diversity 144 | [faq]: https://www.contributor-covenant.org/faq 145 | [translations]: https://www.contributor-covenant.org/translations 146 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Contribute to Ory ladon 5 | 6 | 7 | 8 | 9 | - [Introduction](#introduction) 10 | - [FAQ](#faq) 11 | - [How can I contribute?](#how-can-i-contribute) 12 | - [Communication](#communication) 13 | - [Contribute examples or community projects](#contribute-examples-or-community-projects) 14 | - [Contribute code](#contribute-code) 15 | - [Contribute documentation](#contribute-documentation) 16 | - [Disclosing vulnerabilities](#disclosing-vulnerabilities) 17 | - [Code style](#code-style) 18 | - [Working with forks](#working-with-forks) 19 | - [Conduct](#conduct) 20 | 21 | 22 | 23 | ## Introduction 24 | 25 | _Please note_: We take Ory ladon's security and our users' trust very 26 | seriously. If you believe you have found a security issue in Ory ladon, 27 | please disclose it by contacting us at security@ory.sh. 28 | 29 | There are many ways in which you can contribute. The goal of this document is to 30 | provide a high-level overview of how you can get involved in Ory. 31 | 32 | As a potential contributor, your changes and ideas are welcome at any hour of 33 | the day or night, on weekdays, weekends, and holidays. Please do not ever 34 | hesitate to ask a question or send a pull request. 35 | 36 | If you are unsure, just ask or submit the issue or pull request anyways. You 37 | won't be yelled at for giving it your best effort. The worst that can happen is 38 | that you'll be politely asked to change something. We appreciate any sort of 39 | contributions and don't want a wall of rules to get in the way of that. 40 | 41 | That said, if you want to ensure that a pull request is likely to be merged, 42 | talk to us! You can find out our thoughts and ensure that your contribution 43 | won't clash with Ory 44 | ladon's direction. A great way to 45 | do this is via 46 | [Ory ladon Discussions](https://github.com/orgs/ory/discussions) 47 | or the [Ory Chat](https://www.ory.sh/chat). 48 | 49 | ## FAQ 50 | 51 | - I am new to the community. Where can I find the 52 | [Ory Community Code of Conduct?](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md) 53 | 54 | - I have a question. Where can I get 55 | [answers to questions regarding Ory ladon?](#communication) 56 | 57 | - I would like to contribute but I am not sure how. Are there 58 | [easy ways to contribute?](#how-can-i-contribute) 59 | [Or good first issues?](https://github.com/search?l=&o=desc&q=label%3A%22help+wanted%22+label%3A%22good+first+issue%22+is%3Aopen+user%3Aory+user%3Aory-corp&s=updated&type=Issues) 60 | 61 | - I want to talk to other Ory ladon users. 62 | [How can I become a part of the community?](#communication) 63 | 64 | - I would like to know what I am agreeing to when I contribute to Ory 65 | ladon. 66 | Does Ory have 67 | [a Contributors License Agreement?](https://cla-assistant.io/ory/ladon) 68 | 69 | - I would like updates about new versions of Ory ladon. 70 | [How are new releases announced?](https://www.ory.sh/l/sign-up-newsletter) 71 | 72 | ## How can I contribute? 73 | 74 | If you want to start to contribute code right away, take a look at the 75 | [list of good first issues](https://github.com/ory/ladon/labels/good%20first%20issue). 76 | 77 | There are many other ways you can contribute. Here are a few things you can do 78 | to help out: 79 | 80 | - **Give us a star.** It may not seem like much, but it really makes a 81 | difference. This is something that everyone can do to help out Ory ladon. 82 | Github stars help the project gain visibility and stand out. 83 | 84 | - **Join the community.** Sometimes helping people can be as easy as listening 85 | to their problems and offering a different perspective. Join our Slack, have a 86 | look at discussions in the forum and take part in community events. More info 87 | on this in [Communication](#communication). 88 | 89 | - **Answer discussions.** At all times, there are several unanswered discussions 90 | on GitHub. You can see an 91 | [overview here](https://github.com/discussions?discussions_q=is%3Aunanswered+org%3Aory+sort%3Aupdated-desc). 92 | If you think you know an answer or can provide some information that might 93 | help, please share it! Bonus: You get GitHub achievements for answered 94 | discussions. 95 | 96 | - **Help with open issues.** We have a lot of open issues for Ory ladon and 97 | some of them may lack necessary information, some are duplicates of older 98 | issues. You can help out by guiding people through the process of filling out 99 | the issue template, asking for clarifying information or pointing them to 100 | existing issues that match their description of the problem. 101 | 102 | - **Review documentation changes.** Most documentation just needs a review for 103 | proper spelling and grammar. If you think a document can be improved in any 104 | way, feel free to hit the `edit` button at the top of the page. More info on 105 | contributing to the documentation [here](#contribute-documentation). 106 | 107 | - **Help with tests.** Pull requests may lack proper tests or test plans. These 108 | are needed for the change to be implemented safely. 109 | 110 | ## Communication 111 | 112 | We use [Slack](https://www.ory.sh/chat). You are welcome to drop in and ask 113 | questions, discuss bugs and feature requests, talk to other users of Ory, etc. 114 | 115 | Check out [Ory ladon Discussions](https://github.com/orgs/ory/discussions). This is a great place for 116 | in-depth discussions and lots of code examples, logs and similar data. 117 | 118 | You can also join our community calls if you want to speak to the Ory team 119 | directly or ask some questions. You can find more info and participate in 120 | [Slack](https://www.ory.sh/chat) in the #community-call channel. 121 | 122 | If you want to receive regular notifications about updates to Ory ladon, 123 | consider joining the mailing list. We will _only_ send you vital information on 124 | the projects that you are interested in. 125 | 126 | Also, [follow us on Twitter](https://twitter.com/orycorp). 127 | 128 | ## Contribute examples or community projects 129 | 130 | One of the most impactful ways to contribute is by adding code examples or other 131 | Ory-related code. You can find an overview of community code in the 132 | [awesome-ory](https://github.com/ory/awesome-ory) repository. 133 | 134 | _If you would like to contribute a new example, we would love to hear from you!_ 135 | 136 | Please [open a pull request at awesome-ory](https://github.com/ory/awesome-ory/) 137 | to add your example or Ory-related project to the awesome-ory README. 138 | 139 | ## Contribute code 140 | 141 | Unless you are fixing a known bug, we **strongly** recommend discussing it with 142 | the core team via a GitHub issue or [in our chat](https://www.ory.sh/chat) 143 | before getting started to ensure your work is consistent with Ory ladon's 144 | roadmap and architecture. 145 | 146 | All contributions are made via pull requests. To make a pull request, you will 147 | need a GitHub account; if you are unclear on this process, see GitHub's 148 | documentation on [forking](https://help.github.com/articles/fork-a-repo) and 149 | [pull requests](https://help.github.com/articles/using-pull-requests). Pull 150 | requests should be targeted at the `master` branch. Before creating a pull 151 | request, go through this checklist: 152 | 153 | 1. Create a feature branch off of `master` so that changes do not get mixed up. 154 | 1. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local 155 | changes against the `master` branch. 156 | 1. Run the full project test suite with the `go test -tags sqlite ./...` (or 157 | equivalent) command and confirm that it passes. 158 | 1. Run `make format` 159 | 1. Add a descriptive prefix to commits. This ensures a uniform commit history 160 | and helps structure the changelog. Please refer to this 161 | [Convential Commits configuration](https://github.com/ory/ladon/blob/master/.github/workflows/conventional_commits.yml) 162 | for the list of accepted prefixes. You can read more about the Conventional 163 | Commit specification 164 | [at their site](https://www.conventionalcommits.org/en/v1.0.0/). 165 | 166 | If a pull request is not ready to be reviewed yet 167 | [it should be marked as a "Draft"](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request). 168 | 169 | Before your contributions can be reviewed you need to sign our 170 | [Contributor License Agreement](https://cla-assistant.io/ory/ladon). 171 | 172 | This agreement defines the terms under which your code is contributed to Ory. 173 | More specifically it declares that you have the right to, and actually do, grant 174 | us the rights to use your contribution. You can see the Apache 2.0 license under 175 | which our projects are published 176 | [here](https://github.com/ory/meta/blob/master/LICENSE). 177 | 178 | When pull requests fail the automated testing stages (for example unit or E2E 179 | tests), authors are expected to update their pull requests to address the 180 | failures until the tests pass. 181 | 182 | Pull requests eligible for review 183 | 184 | 1. follow the repository's code formatting conventions; 185 | 2. include tests that prove that the change works as intended and does not add 186 | regressions; 187 | 3. document the changes in the code and/or the project's documentation; 188 | 4. pass the CI pipeline; 189 | 5. have signed our 190 | [Contributor License Agreement](https://cla-assistant.io/ory/ladon); 191 | 6. include a proper git commit message following the 192 | [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/). 193 | 194 | If all of these items are checked, the pull request is ready to be reviewed and 195 | you should change the status to "Ready for review" and 196 | [request review from a maintainer](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review). 197 | 198 | Reviewers will approve the pull request once they are satisfied with the patch. 199 | 200 | ## Contribute documentation 201 | 202 | Please provide documentation when changing, removing, or adding features. All 203 | Ory Documentation resides in the 204 | [Ory documentation repository](https://github.com/ory/docs/). For further 205 | instructions please head over to the Ory Documentation 206 | [README.md](https://github.com/ory/docs/blob/master/README.md). 207 | 208 | ## Disclosing vulnerabilities 209 | 210 | Please disclose vulnerabilities exclusively to 211 | [security@ory.sh](mailto:security@ory.sh). Do not use GitHub issues. 212 | 213 | ## Code style 214 | 215 | Please run `make format` to format all source code following the Ory standard. 216 | 217 | ### Working with forks 218 | 219 | ```bash 220 | # First you clone the original repository 221 | git clone git@github.com:ory/ory/ladon.git 222 | 223 | # Next you add a git remote that is your fork: 224 | git remote add fork git@github.com:/ory/ladon.git 225 | 226 | # Next you fetch the latest changes from origin for master: 227 | git fetch origin 228 | git checkout master 229 | git pull --rebase 230 | 231 | # Next you create a new feature branch off of master: 232 | git checkout my-feature-branch 233 | 234 | # Now you do your work and commit your changes: 235 | git add -A 236 | git commit -a -m "fix: this is the subject line" -m "This is the body line. Closes #123" 237 | 238 | # And the last step is pushing this to your fork 239 | git push -u fork my-feature-branch 240 | ``` 241 | 242 | Now go to the project's GitHub Pull Request page and click "New pull request" 243 | 244 | ## Conduct 245 | 246 | Whether you are a regular contributor or a newcomer, we care about making this 247 | community a safe place for you and we've got your back. 248 | 249 | [Ory Community Code of Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md) 250 | 251 | We welcome discussion about creating a welcoming, safe, and productive 252 | environment for the community. If you have any questions, feedback, or concerns 253 | [please let us know](https://www.ory.sh/chat). 254 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History of breaking changes 2 | 3 | 4 | 5 | 6 | 7 | - [0.7.0](#070) 8 | - [0.6.0](#060) 9 | - [New location](#new-location) 10 | - [Deprecating Redis and RethinkDB](#deprecating-redis-and-rethinkdb) 11 | - [New packages](#new-packages) 12 | - [IMPORTANT: SQL Changes](#important-sql-changes) 13 | - [Manager API Changes](#manager-api-changes) 14 | 15 | 16 | 17 | ## 1.0.0 18 | 19 | The SQL storage implementation has been removed. The reason being that it had serious scalability issues which could 20 | cause trouble in high-throughput environments. 21 | 22 | Everything else stays the same, ORY Ladon is now feature-complete. 23 | 24 | ## 0.8.0 25 | 26 | Managers now must implement `Update(policy Policy) error`. 27 | 28 | ## 0.7.0 29 | 30 | Version 0.7.0 includes two minor BC breaks in the SQLManager. The method signature `CreateSchemas() ( error)` 31 | was changed to `CreateSchemas(schema, table string) (int, error)` where int now returns the number of migrations applied. 32 | Arguments `schema` and `table` are passed to the migration script, defining which schema and table name should be used 33 | to store and look up migration plans. 34 | 35 | To keep the default values from the migrate package, use `CreateSchemas("", "")`. It is safe to reapply all migration 36 | commands with this version - implying that you can choose an arbitrary name and it won't break your schema. 37 | 38 | ## 0.6.0 39 | 40 | Version 0.6.0 includes some larger BC breaks. This version focuses on various 41 | performance boosts for both in-memory and SQL adapters, removes some technical debt 42 | and restructures the repository. 43 | 44 | ### New location 45 | 46 | The location of this library changed from `github.com/ory-am/ladon` to `github.com/ory/ladon`. 47 | 48 | ### Deprecating Redis and RethinkDB 49 | 50 | Redis and RethinkDB are no longer maintained by ORY and were moved to 51 | [ory/ladon-community](https://github.com/ory/ladon-community). The adapters had various 52 | bugs and performance issues which is why they were removed from the official repository. 53 | 54 | ### New packages 55 | 56 | The SQLManager and MemoryManager moved to their own packages in `ladon/manager/sql` and `ladon/manager/memory`. 57 | This change was made to avoid pulling dependencies that are not required by the user. 58 | 59 | ### IMPORTANT: SQL Changes 60 | 61 | The SQLManager was rewritten completely. Now, the database is 3NF (normalized) and includes 62 | various improvements over the previous, naive adapter. The greatest challenge is matching 63 | regular expressions within SQL databases, which causes significant overhead. 64 | 65 | While there is an auto-migration for the schema, the data **is not automatically transferred to 66 | the new schema**. 67 | 68 | However, we provided a migration helper. For usage, check out 69 | [xxx_manager_sql_migrator_test.go](xxx_manager_sql_migrator_test.go) or this short example: 70 | 71 | ```go 72 | var db = getSqlDatabaseFromSomewhere() 73 | s := NewSQLManager(db, nil) 74 | 75 | if err := s.CreateSchemas(); err != nil { 76 | log.Fatalf("Could not create mysql schema: %v", err) 77 | } 78 | 79 | migrator := &SQLManagerMigrateFromMajor0Minor6ToMajor0Minor7{ 80 | DB:db, 81 | SQLManager:s, 82 | } 83 | 84 | err := migrator.Migrate() 85 | ``` 86 | 87 | Please run this migrator **only once and make back ups before you run it**. 88 | 89 | ### Manager API Changes 90 | 91 | `Manager.FindPoliciesForSubject` is now `Manager.FindRequestCandidates` 92 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please use issues only to raise potential bugs or request features. For everything else ask 2 | the [ORY Community](https://community.ory.am/) or join the [ORY Chat](https://gitter.im/ory-am/hydra). 3 | 4 | If you think you found a security vulnerability, please refrain from posting it publicly on the forums, the chat, or GitHub 5 | and send us an email to [hi@ory.am](mailto:hi@ory.am) instead. 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Aeneas Rekkas (github: arekkas) 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Ory Security Policy 5 | 6 | This policy outlines Ory's security commitments and practices for users across 7 | different licensing and deployment models. 8 | 9 | To learn more about Ory's security service level agreements (SLAs) and 10 | processes, please [contact us](https://www.ory.sh/contact/). 11 | 12 | ## Ory Network Users 13 | 14 | - **Security SLA:** Ory addresses vulnerabilities in the Ory Network according 15 | to the following guidelines: 16 | - Critical: Typically addressed within 14 days. 17 | - High: Typically addressed within 30 days. 18 | - Medium: Typically addressed within 90 days. 19 | - Low: Typically addressed within 180 days. 20 | - Informational: Addressed as necessary. 21 | These timelines are targets and may vary based on specific circumstances. 22 | - **Release Schedule:** Updates are deployed to the Ory Network as 23 | vulnerabilities are resolved. 24 | - **Version Support:** The Ory Network always runs the latest version, ensuring 25 | up-to-date security fixes. 26 | 27 | ## Ory Enterprise License Customers 28 | 29 | - **Security SLA:** Ory addresses vulnerabilities based on their severity: 30 | - Critical: Typically addressed within 14 days. 31 | - High: Typically addressed within 30 days. 32 | - Medium: Typically addressed within 90 days. 33 | - Low: Typically addressed within 180 days. 34 | - Informational: Addressed as necessary. 35 | These timelines are targets and may vary based on specific circumstances. 36 | - **Release Schedule:** Updates are made available as vulnerabilities are 37 | resolved. Ory works closely with enterprise customers to ensure timely updates 38 | that align with their operational needs. 39 | - **Version Support:** Ory may provide security support for multiple versions, 40 | depending on the terms of the enterprise agreement. 41 | 42 | ## Apache 2.0 License Users 43 | 44 | - **Security SLA:** Ory does not provide a formal SLA for security issues under 45 | the Apache 2.0 License. 46 | - **Release Schedule:** Releases prioritize new functionality and include fixes 47 | for known security vulnerabilities at the time of release. While major 48 | releases typically occur one to two times per year, Ory does not guarantee a 49 | fixed release schedule. 50 | - **Version Support:** Security patches are only provided for the latest release 51 | version. 52 | 53 | ## Reporting a Vulnerability 54 | 55 | For details on how to report security vulnerabilities, visit our 56 | [security policy documentation](https://www.ory.sh/docs/ecosystem/security). 57 | -------------------------------------------------------------------------------- /audit_logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // AuditLogger tracks denied and granted authorizations. 26 | type AuditLogger interface { 27 | LogRejectedAccessRequest(ctx context.Context, request *Request, pool Policies, deciders Policies) 28 | LogGrantedAccessRequest(ctx context.Context, request *Request, pool Policies, deciders Policies) 29 | } 30 | -------------------------------------------------------------------------------- /audit_logger_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "log" 26 | "os" 27 | "strings" 28 | ) 29 | 30 | // AuditLoggerInfo outputs information about granting or rejecting policies. 31 | type AuditLoggerInfo struct { 32 | Logger *log.Logger 33 | } 34 | 35 | func (a *AuditLoggerInfo) logger() *log.Logger { 36 | if a.Logger == nil { 37 | a.Logger = log.New(os.Stderr, "", log.LstdFlags) 38 | } 39 | return a.Logger 40 | } 41 | 42 | func (a *AuditLoggerInfo) LogRejectedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { 43 | if len(d) > 1 { 44 | allowed := joinPoliciesNames(d[0 : len(d)-1]) 45 | denied := d[len(d)-1].GetID() 46 | a.logger().Printf("policies %s allow access, but policy %s forcefully denied it", allowed, denied) 47 | } else if len(d) == 1 { 48 | denied := d[len(d)-1].GetID() 49 | a.logger().Printf("policy %s forcefully denied the access", denied) 50 | } else { 51 | a.logger().Printf("no policy allowed access") 52 | } 53 | } 54 | 55 | func (a *AuditLoggerInfo) LogGrantedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { 56 | a.logger().Printf("policies %s allow access", joinPoliciesNames(d)) 57 | } 58 | 59 | func joinPoliciesNames(policies Policies) string { 60 | names := []string{} 61 | for _, policy := range policies { 62 | names = append(names, policy.GetID()) 63 | } 64 | return strings.Join(names, ", ") 65 | } 66 | -------------------------------------------------------------------------------- /audit_logger_noop.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // AuditLoggerNoOp is the default AuditLogger, that tracks nothing. 26 | type AuditLoggerNoOp struct{} 27 | 28 | func (*AuditLoggerNoOp) LogRejectedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { 29 | } 30 | func (*AuditLoggerNoOp) LogGrantedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { 31 | } 32 | 33 | var DefaultAuditLogger = &AuditLoggerNoOp{} 34 | -------------------------------------------------------------------------------- /audit_logger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "bytes" 25 | "context" 26 | "log" 27 | "testing" 28 | 29 | "github.com/stretchr/testify/assert" 30 | 31 | . "github.com/ory/ladon" 32 | . "github.com/ory/ladon/manager/memory" 33 | ) 34 | 35 | func TestAuditLogger(t *testing.T) { 36 | var output bytes.Buffer 37 | 38 | warden := &Ladon{ 39 | Manager: NewMemoryManager(), 40 | AuditLogger: &AuditLoggerInfo{ 41 | Logger: log.New(&output, "", 0), 42 | }, 43 | } 44 | 45 | ctx := context.Background() 46 | 47 | warden.Manager.Create(ctx, &DefaultPolicy{ 48 | ID: "no-updates", 49 | Subjects: []string{"<.*>"}, 50 | Actions: []string{"update"}, 51 | Resources: []string{"<.*>"}, 52 | Effect: DenyAccess, 53 | }) 54 | warden.Manager.Create(ctx, &DefaultPolicy{ 55 | ID: "yes-deletes", 56 | Subjects: []string{"<.*>"}, 57 | Actions: []string{"delete"}, 58 | Resources: []string{"<.*>"}, 59 | Effect: AllowAccess, 60 | }) 61 | warden.Manager.Create(ctx, &DefaultPolicy{ 62 | ID: "no-bob", 63 | Subjects: []string{"bob"}, 64 | Actions: []string{"delete"}, 65 | Resources: []string{"<.*>"}, 66 | Effect: DenyAccess, 67 | }) 68 | 69 | r := &Request{} 70 | assert.NotNil(t, warden.IsAllowed(ctx, r)) 71 | assert.Equal(t, "no policy allowed access\n", output.String()) 72 | 73 | output.Reset() 74 | 75 | r = &Request{ 76 | Action: "update", 77 | } 78 | assert.NotNil(t, warden.IsAllowed(ctx, r)) 79 | assert.Equal(t, "policy no-updates forcefully denied the access\n", output.String()) 80 | 81 | output.Reset() 82 | 83 | r = &Request{ 84 | Subject: "bob", 85 | Action: "delete", 86 | } 87 | assert.NotNil(t, warden.IsAllowed(ctx, r)) 88 | assert.Equal(t, "policies yes-deletes allow access, but policy no-bob forcefully denied it\n", output.String()) 89 | 90 | output.Reset() 91 | 92 | r = &Request{ 93 | Subject: "alice", 94 | Action: "delete", 95 | } 96 | assert.Nil(t, warden.IsAllowed(ctx, r)) 97 | assert.Equal(t, "policies yes-deletes allow access\n", output.String()) 98 | } 99 | -------------------------------------------------------------------------------- /benchmark_warden_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "strconv" 27 | "testing" 28 | 29 | "github.com/pborman/uuid" 30 | "github.com/pkg/errors" 31 | 32 | "github.com/ory/ladon" 33 | "github.com/ory/ladon/manager/memory" 34 | ) 35 | 36 | func benchmarkLadon(i int, b *testing.B, warden *ladon.Ladon) { 37 | //var concurrency = 30 38 | //var sem = make(chan bool, concurrency) 39 | // 40 | //for _, pol := range generatePolicies(i) { 41 | // sem <- true 42 | // go func(pol ladon.Policy) { 43 | // defer func() { <-sem }() 44 | // if err := warden.Manager.Create(pol); err != nil { 45 | // b.Logf("Got error from warden.Manager.Create: %s", err) 46 | // } 47 | // }(pol) 48 | //} 49 | // 50 | //for i := 0; i < cap(sem); i++ { 51 | // sem <- true 52 | //} 53 | 54 | ctx := context.Background() 55 | 56 | for _, pol := range generatePolicies(i) { 57 | if err := warden.Manager.Create(ctx, pol); err != nil { 58 | b.Logf("Got error from warden.Manager.Create: %s", err) 59 | } 60 | } 61 | 62 | b.ResetTimer() 63 | var err error 64 | for n := 0; n < b.N; n++ { 65 | if err = warden.IsAllowed(ctx, &ladon.Request{ 66 | Subject: "5", 67 | Action: "bar", 68 | Resource: "baz", 69 | }); errors.Cause(err) == ladon.ErrRequestDenied || errors.Cause(err) == ladon.ErrRequestForcefullyDenied || err == nil { 70 | } else { 71 | b.Logf("Got error from warden: %s", err) 72 | } 73 | } 74 | } 75 | 76 | func BenchmarkLadon(b *testing.B) { 77 | for _, num := range []int{10, 100, 1000, 10000, 100000, 1000000} { 78 | b.Run(fmt.Sprintf("store=memory/policies=%d", num), func(b *testing.B) { 79 | matcher := ladon.NewRegexpMatcher(4096) 80 | benchmarkLadon(num, b, &ladon.Ladon{ 81 | Manager: memory.NewMemoryManager(), 82 | Matcher: matcher, 83 | }) 84 | }) 85 | 86 | b.Run(fmt.Sprintf("store=mysql/policies=%d", num), func(b *testing.B) { 87 | benchmarkLadon(num, b, &ladon.Ladon{ 88 | Manager: managers["mysql"], 89 | Matcher: ladon.NewRegexpMatcher(4096), 90 | }) 91 | }) 92 | 93 | b.Run(fmt.Sprintf("store=postgres/policies=%d", num), func(b *testing.B) { 94 | benchmarkLadon(num, b, &ladon.Ladon{ 95 | Manager: managers["postgres"], 96 | Matcher: ladon.NewRegexpMatcher(4096), 97 | }) 98 | }) 99 | } 100 | } 101 | 102 | func generatePolicies(n int) map[string]ladon.Policy { 103 | policies := map[string]ladon.Policy{} 104 | for i := 0; i <= n; i++ { 105 | id := uuid.New() 106 | policies[id] = &ladon.DefaultPolicy{ 107 | ID: id, 108 | Subjects: []string{"foobar", "some-resource" + fmt.Sprintf("%d", i%100), strconv.Itoa(i)}, 109 | Actions: []string{"foobar", "foobar", "foobar", "foobar", "foobar"}, 110 | Resources: []string{"foobar", id}, 111 | Effect: ladon.AllowAccess, 112 | } 113 | } 114 | return policies 115 | } 116 | -------------------------------------------------------------------------------- /compiler/README.md: -------------------------------------------------------------------------------- 1 | # compiler 2 | 3 | [Read the docs.](https://godoc.org/github.com/ory-am/common/compiler) -------------------------------------------------------------------------------- /compiler/regex.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | // Package compiler offers a regexp compiler which compiles regex templates to regexp.Regexp 22 | // 23 | // reg, err := compiler.CompileRegex("foo:bar.baz:<[0-9]{2,10}>", '<', '>') 24 | // // if err != nil ... 25 | // reg.MatchString("foo:bar.baz:123") 26 | // 27 | // reg, err := compiler.CompileRegex("/foo/bar/url/{[a-z]+}", '{', '}') 28 | // // if err != nil ... 29 | // reg.MatchString("/foo/bar/url/abz") 30 | // 31 | // This package is adapts github.com/gorilla/mux/regexp.go 32 | 33 | package compiler 34 | 35 | // Copyright 2012 The Gorilla Authors. All rights reserved. 36 | // Use of this source code is governed by a BSD-style 37 | // license as follows: 38 | 39 | // Copyright (c) 2012 Rodrigo Moraes. All rights reserved. 40 | // 41 | // Redistribution and use in source and binary forms, with or without 42 | // modification, are permitted provided that the following conditions are 43 | // met: 44 | // 45 | // * Redistributions of source code must retain the above copyright 46 | // notice, this list of conditions and the following disclaimer. 47 | // * Redistributions in binary form must reproduce the above 48 | // copyright notice, this list of conditions and the following disclaimer 49 | // in the documentation and/or other materials provided with the 50 | // distribution. 51 | // * Neither the name of Google Inc. nor the names of its 52 | // contributors may be used to endorse or promote products derived from 53 | // this software without specific prior written permission. 54 | // 55 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 56 | // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 57 | // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 58 | // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 59 | // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 60 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 61 | // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 62 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 63 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 64 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 65 | // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 66 | 67 | import ( 68 | "bytes" 69 | "fmt" 70 | "regexp" 71 | "time" 72 | 73 | "github.com/dlclark/regexp2" 74 | ) 75 | 76 | const regexp2MatchTimeout = time.Millisecond * 250 77 | 78 | // delimiterIndices returns the first level delimiter indices from a string. 79 | // It returns an error in case of unbalanced delimiters. 80 | func delimiterIndices(s string, delimiterStart, delimiterEnd byte) ([]int, error) { 81 | var level, idx int 82 | idxs := make([]int, 0) 83 | for i := 0; i < len(s); i++ { 84 | switch s[i] { 85 | case delimiterStart: 86 | if level++; level == 1 { 87 | idx = i 88 | } 89 | case delimiterEnd: 90 | if level--; level == 0 { 91 | idxs = append(idxs, idx, i+1) 92 | } else if level < 0 { 93 | return nil, fmt.Errorf(`Unbalanced braces in "%q"`, s) 94 | } 95 | } 96 | } 97 | 98 | if level != 0 { 99 | return nil, fmt.Errorf(`Unbalanced braces in "%q"`, s) 100 | } 101 | 102 | return idxs, nil 103 | } 104 | 105 | // CompileRegex parses a template and returns a Regexp. 106 | // 107 | // You can define your own delimiters. It is e.g. common to use curly braces {} but I recommend using characters 108 | // which have no special meaning in Regex, e.g.: <, > 109 | // 110 | // reg, err := compiler.CompileRegex("foo:bar.baz:<[0-9]{2,10}>", '<', '>') 111 | // // if err != nil ... 112 | // reg.MatchString("foo:bar.baz:123") 113 | func CompileRegex(tpl string, delimiterStart, delimiterEnd byte) (*regexp2.Regexp, error) { 114 | // Check if it is well-formed. 115 | idxs, errBraces := delimiterIndices(tpl, delimiterStart, delimiterEnd) 116 | if errBraces != nil { 117 | return nil, errBraces 118 | } 119 | varsR := make([]*regexp2.Regexp, len(idxs)/2) 120 | pattern := bytes.NewBufferString("") 121 | pattern.WriteByte('^') 122 | 123 | var end int 124 | for i := 0; i < len(idxs); i += 2 { 125 | // Set all values we are interested in. 126 | raw := tpl[end:idxs[i]] 127 | end = idxs[i+1] 128 | patt := tpl[idxs[i]+1 : end-1] 129 | // Build the regexp pattern. 130 | varIdx := i / 2 131 | fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) 132 | reg, err := regexp2.Compile(fmt.Sprintf("^%s$", patt), regexp2.RE2) 133 | if err != nil { 134 | return nil, err 135 | } 136 | reg.MatchTimeout = regexp2MatchTimeout 137 | varsR[varIdx] = reg 138 | } 139 | 140 | // Add the remaining. 141 | raw := tpl[end:] 142 | pattern.WriteString(regexp.QuoteMeta(raw)) 143 | pattern.WriteByte('$') 144 | 145 | // Compile full regexp. 146 | reg, errCompile := regexp2.Compile(pattern.String(), regexp2.RE2) 147 | if errCompile != nil { 148 | return nil, errCompile 149 | } 150 | reg.MatchTimeout = regexp2MatchTimeout 151 | 152 | return reg, nil 153 | } 154 | -------------------------------------------------------------------------------- /compiler/regex_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package compiler 22 | 23 | import ( 24 | "testing" 25 | 26 | "github.com/dlclark/regexp2" 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestRegexCompiler(t *testing.T) { 31 | for k, c := range []struct { 32 | template string 33 | delimiterStart byte 34 | delimiterEnd byte 35 | failCompile bool 36 | matchAgainst string 37 | failMatch bool 38 | }{ 39 | {"urn:foo:{.*}", '{', '}', false, "urn:foo:bar:baz", false}, 40 | {"urn:foo.bar.com:{.*}", '{', '}', false, "urn:foo.bar.com:bar:baz", false}, 41 | {"urn:foo.bar.com:{.*}", '{', '}', false, "urn:foo.com:bar:baz", true}, 42 | {"urn:foo.bar.com:{.*}", '{', '}', false, "foobar", true}, 43 | {"urn:foo.bar.com:{.{1,2}}", '{', '}', false, "urn:foo.bar.com:aa", false}, 44 | 45 | {"urn:foo.bar.com:{.*{}", '{', '}', true, "", true}, 46 | {"urn:foo:<.*>", '<', '>', false, "urn:foo:bar:baz", false}, 47 | 48 | {`urn:foo:`, '<', '>', false, "urn:foo:user=john", false}, 49 | {`urn:foo:`, '<', '>', false, "urn:foo:user=admin", true}, 50 | 51 | {`urn:foo:user=<[[:digit:]]*>`, '<', '>', false, "urn:foo:user=admin", true}, 52 | {`urn:foo:user=<[[:digit:]]*>`, '<', '>', false, "urn:foo:user=62235", false}, 53 | 54 | {`urn:foo:user={(?P\d{3})}`, '{', '}', false, "urn:foo:user=622", false}, 55 | {`urn:foo:user=<(?P\d{3})>`, '<', '>', false, "urn:foo:user=622", false}, 56 | {`urn:foo:user=<(?P\d{3})>`, '<', '>', false, "urn:foo:user=aaa", true}, 57 | 58 | // Ignoring this case for now... 59 | // {"urn:foo.bar.com:{.*\\{}", '{', '}', false, "", true}, 60 | } { 61 | k++ 62 | result, err := CompileRegex(c.template, c.delimiterStart, c.delimiterEnd) 63 | assert.Equal(t, c.failCompile, err != nil, "Case %d", k) 64 | if c.failCompile || err != nil { 65 | continue 66 | } 67 | 68 | t.Logf("Case %d compiled to: %s", k, result.String()) 69 | re := regexp2.MustCompile(result.String(), regexp2.RE2) 70 | ok, err := re.MatchString(c.matchAgainst) 71 | assert.Nil(t, err, "Case %d", k) 72 | assert.Equal(t, !c.failMatch, ok, "Case %d", k) 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /condition.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "encoding/json" 26 | 27 | "github.com/pkg/errors" 28 | ) 29 | 30 | // Condition either do or do not fulfill an access request. 31 | type Condition interface { 32 | // GetName returns the condition's name. 33 | GetName() string 34 | 35 | // Fulfills returns true if the request is fulfilled by the condition. 36 | Fulfills(context.Context, interface{}, *Request) bool 37 | } 38 | 39 | // Conditions is a collection of conditions. 40 | type Conditions map[string]Condition 41 | 42 | // AddCondition adds a condition to the collection. 43 | func (cs Conditions) AddCondition(key string, c Condition) { 44 | cs[key] = c 45 | } 46 | 47 | // MarshalJSON marshals a list of conditions to json. 48 | func (cs Conditions) MarshalJSON() ([]byte, error) { 49 | out := make(map[string]*jsonCondition, len(cs)) 50 | for k, c := range cs { 51 | raw, err := json.Marshal(c) 52 | if err != nil { 53 | return []byte{}, errors.WithStack(err) 54 | } 55 | 56 | out[k] = &jsonCondition{ 57 | Type: c.GetName(), 58 | Options: json.RawMessage(raw), 59 | } 60 | } 61 | 62 | return json.Marshal(out) 63 | } 64 | 65 | // UnmarshalJSON unmarshals a list of conditions from json. 66 | func (cs Conditions) UnmarshalJSON(data []byte) error { 67 | if cs == nil { 68 | return errors.New("Can not be nil") 69 | } 70 | 71 | var jcs map[string]jsonCondition 72 | var dc Condition 73 | 74 | if err := json.Unmarshal(data, &jcs); err != nil { 75 | return errors.WithStack(err) 76 | } 77 | 78 | for k, jc := range jcs { 79 | var found bool 80 | for name, c := range ConditionFactories { 81 | if name == jc.Type { 82 | found = true 83 | dc = c() 84 | 85 | if len(jc.Options) == 0 { 86 | cs[k] = dc 87 | break 88 | } 89 | 90 | if err := json.Unmarshal(jc.Options, dc); err != nil { 91 | return errors.WithStack(err) 92 | } 93 | 94 | cs[k] = dc 95 | break 96 | } 97 | } 98 | 99 | if !found { 100 | return errors.Errorf("Could not find condition type %s", jc.Type) 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | 107 | type jsonCondition struct { 108 | Type string `json:"type"` 109 | Options json.RawMessage `json:"options"` 110 | } 111 | 112 | // ConditionFactories is where you can add custom conditions 113 | var ConditionFactories = map[string]func() Condition{ 114 | new(StringEqualCondition).GetName(): func() Condition { 115 | return new(StringEqualCondition) 116 | }, 117 | new(CIDRCondition).GetName(): func() Condition { 118 | return new(CIDRCondition) 119 | }, 120 | new(EqualsSubjectCondition).GetName(): func() Condition { 121 | return new(EqualsSubjectCondition) 122 | }, 123 | new(StringPairsEqualCondition).GetName(): func() Condition { 124 | return new(StringPairsEqualCondition) 125 | }, 126 | new(StringMatchCondition).GetName(): func() Condition { 127 | return new(StringMatchCondition) 128 | }, 129 | new(ResourceContainsCondition).GetName(): func() Condition { 130 | return new(ResourceContainsCondition) 131 | }, 132 | new(BooleanCondition).GetName(): func() Condition { 133 | return new(BooleanCondition) 134 | }, 135 | } 136 | -------------------------------------------------------------------------------- /condition_boolean.go: -------------------------------------------------------------------------------- 1 | package ladon 2 | 3 | import "context" 4 | 5 | /* 6 | BooleanCondition is used to determine if a boolean context matches an expected 7 | boolean condition. 8 | 9 | BooleanCondition implements the ladon.Condition interface. 10 | See https://github.com/ory/ladon/blob/master/condition.go 11 | */ 12 | type BooleanCondition struct { 13 | BooleanValue bool `json:"value"` 14 | } 15 | 16 | // GetName returns the name of the BooleanCondition 17 | func (c *BooleanCondition) GetName() string { 18 | return "BooleanCondition" 19 | } 20 | 21 | // Fulfills determines if the BooleanCondition is fulfilled. 22 | // The BooleanCondition is fulfilled if the provided boolean value matches the conditions boolean value. 23 | func (c *BooleanCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { 24 | val, ok := value.(bool) 25 | 26 | return ok && val == c.BooleanValue 27 | } 28 | -------------------------------------------------------------------------------- /condition_cidr.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "net" 26 | ) 27 | 28 | // CIDRCondition makes sure that the warden requests' IP address is in the given CIDR. 29 | type CIDRCondition struct { 30 | CIDR string `json:"cidr"` 31 | } 32 | 33 | // Fulfills returns true if the the request is fulfilled by the condition. 34 | func (c *CIDRCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { 35 | ips, ok := value.(string) 36 | if !ok { 37 | return false 38 | } 39 | 40 | _, cidrnet, err := net.ParseCIDR(c.CIDR) 41 | if err != nil { 42 | return false 43 | } 44 | 45 | ip := net.ParseIP(ips) 46 | if ip == nil { 47 | return false 48 | } 49 | 50 | return cidrnet.Contains(ip) 51 | } 52 | 53 | // GetName returns the condition's name. 54 | func (c *CIDRCondition) GetName() string { 55 | return "CIDRCondition" 56 | } 57 | -------------------------------------------------------------------------------- /condition_cidr_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestCIDRMatch(t *testing.T) { 31 | for _, c := range []struct { 32 | cidr string 33 | ip string 34 | pass bool 35 | }{ 36 | {ip: "192.168.1.67", cidr: "192.168.1.0/24", pass: true}, 37 | {ip: "192.168.1.67", cidr: "192.168.1.0/28", pass: false}, 38 | {ip: "192.168.1.67", cidr: "1", pass: false}, 39 | {ip: "1", cidr: "192.168.1.0/28", pass: false}, 40 | {ip: "192.168.1.67", cidr: "0.0.0.0/0", pass: true}, 41 | } { 42 | condition := &CIDRCondition{ 43 | CIDR: c.cidr, 44 | } 45 | 46 | assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.ip, new(Request)), "%s; %s", c.ip, c.cidr) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /condition_resource_contains.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "strings" 26 | ) 27 | 28 | // ResourceContainsCondition is fulfilled if the context matches a substring within the resource name 29 | type ResourceContainsCondition struct{} 30 | 31 | // Fulfills returns true if the request's resouce contains the given value string 32 | func (c *ResourceContainsCondition) Fulfills(ctx context.Context, value interface{}, r *Request) bool { 33 | 34 | filter, ok := value.(map[string]interface{}) 35 | if !ok { 36 | return false 37 | } 38 | 39 | valueString, ok := filter["value"].(string) 40 | if !ok || len(valueString) < 1 { 41 | return false 42 | } 43 | 44 | //If no delimiter provided default to "equals" check 45 | delimiterString, ok := filter["delimiter"].(string) 46 | if !ok || len(delimiterString) < 1 { 47 | delimiterString = "" 48 | } 49 | 50 | // Append delimiter to strings to prevent delim+1 being interpreted as delim+10 being present 51 | filterValue := delimiterString + valueString + delimiterString 52 | resourceString := delimiterString + r.Resource + delimiterString 53 | 54 | matches := strings.Contains(resourceString, filterValue) 55 | return matches 56 | 57 | } 58 | 59 | // GetName returns the condition's name. 60 | func (c *ResourceContainsCondition) GetName() string { 61 | return "ResourceContainsCondition" 62 | } 63 | -------------------------------------------------------------------------------- /condition_resource_contains_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | // Licensed under the Apache License, Version 2.0 (the "License"); 22 | // you may not use this file except in compliance with the License. 23 | // You may obtain a copy of the License at 24 | // 25 | // http://www.apache.org/licenses/LICENSE-2.0 26 | // 27 | // Unless required by applicable law or agreed to in writing, software 28 | // distributed under the License is distributed on an "AS IS" BASIS, 29 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 30 | // See the License for the specific language governing permissions and 31 | // limitations under the License. 32 | 33 | package ladon 34 | 35 | import ( 36 | "context" 37 | "testing" 38 | 39 | "github.com/stretchr/testify/assert" 40 | ) 41 | 42 | func TestResourceContains(t *testing.T) { 43 | for _, c := range []struct { 44 | matches string 45 | delimiter string 46 | resource string 47 | value string 48 | pass bool 49 | }{ 50 | //Correct matching within resource string with delimiter: 51 | {matches: "Find value fails incomplete resource string parts.", delimiter: ":", value: "foo:ba", resource: "foo:bar", pass: false}, 52 | 53 | {matches: "Find value as equal to a resource string", delimiter: ":", value: "foo:bar", resource: "foo:bar", pass: true}, 54 | {matches: "Find value as prefix of resource string", delimiter: ":", value: "foo:bar", resource: "foo:bar:baz", pass: true}, 55 | {matches: "Find value not being prefix of resource string", delimiter: ":", value: "foo:baz", resource: "foo:bar:baz", pass: false}, 56 | {matches: "Find value as afix of resource string", delimiter: ":", value: "bar:baz", resource: "foo:bar:baz", pass: true}, 57 | {matches: "Find value not being afix of resource string", delimiter: ":", value: "foo:baz", resource: "foo:bar:baz", pass: false}, 58 | {matches: "Find value as in middle of resource string", delimiter: ":", value: "bar", resource: "foo:bar:baz", pass: true}, 59 | {matches: "Find value not being in middle of resource string", delimiter: ":", value: "bar", resource: "baz:foo:baz", pass: false}, 60 | 61 | //Correct matching within resource string without delimiter: 62 | {matches: "Find value does not work for part incomplete resource string parts without delimiter.", value: "foo:ba", resource: "foo:bar", pass: true}, 63 | {matches: "Find value as equal to a resource string", value: "foo:bar", resource: "foo:bar", pass: true}, 64 | {matches: "Find value as prefix of resource string", value: "foo:bar", resource: "foo:bar:baz", pass: true}, 65 | {matches: "Find value not being prefix of resource string", value: "foo:baz", resource: "foo:bar:baz", pass: false}, 66 | {matches: "Find value as afix of resource string", value: "bar:baz", resource: "foo:bar:baz", pass: true}, 67 | {matches: "Find value not being afix of resource string", value: "foo:baz", resource: "foo:bar:baz", pass: false}, 68 | {matches: "Find value as in middle of resource string", value: "bar", resource: "foo:bar:baz", pass: true}, 69 | {matches: "Find value not being in middle of resource string", value: "bar", resource: "baz:foo:baz", pass: false}, 70 | 71 | //Erroneous requests: 72 | {matches: "value missing from request", delimiter: ":", value: "", resource: "abc", pass: false}, 73 | } { 74 | condition := &ResourceContainsCondition{} 75 | 76 | ctx := make(Context) 77 | request := &Request{Resource: c.resource, Subject: "users:arneanka", Context: ctx} 78 | 79 | //The context: 80 | resourceFilter := make(map[string]interface{}) 81 | if len(c.delimiter) > 0 { 82 | resourceFilter["delimiter"] = c.delimiter 83 | } 84 | resourceFilter["value"] = c.value 85 | 86 | assert.Equal(t, c.pass, condition.Fulfills(context.Background(), resourceFilter, request), "%s", c.matches) 87 | assert.Equal(t, condition.GetName(), "ResourceContainsCondition", "should be called ResourceContainsCondition") 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /condition_string_equal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // StringEqualCondition is a condition which is fulfilled if the given 26 | // string value is the same as specified in StringEqualCondition 27 | type StringEqualCondition struct { 28 | Equals string `json:"equals"` 29 | } 30 | 31 | // Fulfills returns true if the given value is a string and is the 32 | // same as in StringEqualCondition.Equals 33 | func (c *StringEqualCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { 34 | s, ok := value.(string) 35 | 36 | return ok && s == c.Equals 37 | } 38 | 39 | // GetName returns the condition's name. 40 | func (c *StringEqualCondition) GetName() string { 41 | return "StringEqualCondition" 42 | } 43 | -------------------------------------------------------------------------------- /condition_string_match.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "regexp" 26 | ) 27 | 28 | // StringMatchCondition is a condition which is fulfilled if the given 29 | // string value matches the regex pattern specified in StringMatchCondition 30 | type StringMatchCondition struct { 31 | Matches string `json:"matches"` 32 | } 33 | 34 | // Fulfills returns true if the given value is a string and matches the regex 35 | // pattern in StringMatchCondition.Matches 36 | func (c *StringMatchCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { 37 | s, ok := value.(string) 38 | 39 | matches, _ := regexp.MatchString(c.Matches, s) 40 | 41 | return ok && matches 42 | } 43 | 44 | // GetName returns the condition's name. 45 | func (c *StringMatchCondition) GetName() string { 46 | return "StringMatchCondition" 47 | } 48 | -------------------------------------------------------------------------------- /condition_string_match_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestStringMatch(t *testing.T) { 31 | for _, c := range []struct { 32 | matches string 33 | value interface{} 34 | pass bool 35 | }{ 36 | {matches: ".*", value: "abc", pass: true}, 37 | {matches: "abc.*", value: "abc", pass: true}, 38 | {matches: "abc.+", value: "abc", pass: false}, 39 | } { 40 | condition := &StringMatchCondition{ 41 | Matches: c.matches, 42 | } 43 | 44 | assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.value, new(Request)), "%s", c.matches) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /condition_string_pairs_equal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // StringPairsEqualCondition is a condition which is fulfilled if the given 26 | // array of pairs contains two-element string arrays where both elements 27 | // in the string array are equal 28 | type StringPairsEqualCondition struct{} 29 | 30 | // Fulfills returns true if the given value is an array of string arrays and 31 | // each string array has exactly two values which are equal 32 | func (c *StringPairsEqualCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { 33 | pairs, PairsOk := value.([]interface{}) 34 | if !PairsOk { 35 | return false 36 | } 37 | 38 | for _, v := range pairs { 39 | pair, PairOk := v.([]interface{}) 40 | if !PairOk || (len(pair) != 2) { 41 | return false 42 | } 43 | 44 | a, AOk := pair[0].(string) 45 | b, BOk := pair[1].(string) 46 | 47 | if !AOk || !BOk || (a != b) { 48 | return false 49 | } 50 | } 51 | 52 | return true 53 | } 54 | 55 | // GetName returns the condition's name. 56 | func (c *StringPairsEqualCondition) GetName() string { 57 | return "StringPairsEqualCondition" 58 | } 59 | -------------------------------------------------------------------------------- /condition_string_pairs_equal_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestStringPairsEqualMatch(t *testing.T) { 31 | for _, c := range []struct { 32 | pairs interface{} 33 | pass bool 34 | }{ 35 | {pairs: "junk", pass: false}, 36 | {pairs: []interface{}{[]interface{}{}}, pass: false}, 37 | {pairs: []interface{}{[]interface{}{"1"}}, pass: false}, 38 | {pairs: []interface{}{[]interface{}{"1", "1", "2"}}, pass: false}, 39 | {pairs: []interface{}{[]interface{}{"1", "2"}}, pass: false}, 40 | {pairs: []interface{}{[]interface{}{"1", "1"}, []interface{}{"2", "3"}}, pass: false}, 41 | {pairs: []interface{}{}, pass: true}, 42 | {pairs: []interface{}{[]interface{}{"1", "1"}}, pass: true}, 43 | {pairs: []interface{}{[]interface{}{"1", "1"}, []interface{}{"2", "2"}}, pass: true}, 44 | } { 45 | condition := &StringPairsEqualCondition{} 46 | 47 | assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.pairs, new(Request)), "%s", c.pairs) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /condition_subject_equal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // EqualsSubjectCondition is a condition which is fulfilled if the request's subject is equal to the given value string 26 | type EqualsSubjectCondition struct{} 27 | 28 | // Fulfills returns true if the request's subject is equal to the given value string 29 | func (c *EqualsSubjectCondition) Fulfills(ctx context.Context, value interface{}, r *Request) bool { 30 | s, ok := value.(string) 31 | 32 | return ok && s == r.Subject 33 | } 34 | 35 | // GetName returns the condition's name. 36 | func (c *EqualsSubjectCondition) GetName() string { 37 | return "EqualsSubjectCondition" 38 | } 39 | -------------------------------------------------------------------------------- /condition_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "encoding/json" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | ) 30 | 31 | func TestConditionsAppend(t *testing.T) { 32 | cs := Conditions{} 33 | c := &CIDRCondition{} 34 | cs.AddCondition("clientIP", c) 35 | assert.Equal(t, c, cs["clientIP"]) 36 | } 37 | 38 | func TestMarshalUnmarshalNative(t *testing.T) { 39 | css := &Conditions{ 40 | "clientIP": &CIDRCondition{CIDR: "127.0.0.1/0"}, 41 | "owner": &EqualsSubjectCondition{}, 42 | } 43 | out, err := json.Marshal(css) 44 | require.Nil(t, err) 45 | t.Logf("%s", out) 46 | 47 | cs := Conditions{} 48 | require.Nil(t, cs.UnmarshalJSON(out)) 49 | } 50 | 51 | func TestMarshalUnmarshal(t *testing.T) { 52 | css := &Conditions{ 53 | "clientIP": &CIDRCondition{CIDR: "127.0.0.1/0"}, 54 | "owner": &EqualsSubjectCondition{}, 55 | "role": &StringMatchCondition{Matches: ".*"}, 56 | "hasElevatedPrivileges": &BooleanCondition{BooleanValue: true}, 57 | } 58 | out, err := json.Marshal(css) 59 | require.Nil(t, err) 60 | t.Logf("%s", out) 61 | 62 | cs := Conditions{} 63 | require.Nil(t, json.Unmarshal([]byte(`{ 64 | "owner": { 65 | "type": "EqualsSubjectCondition" 66 | }, 67 | "clientIP": { 68 | "type": "CIDRCondition", 69 | "options": { 70 | "cidr": "127.0.0.1/0" 71 | } 72 | }, 73 | "role": { 74 | "type": "StringMatchCondition", 75 | "options": { 76 | "matches": ".*" 77 | } 78 | }, 79 | "hasElevatedPrivileges": { 80 | "type": "BooleanCondition", 81 | "options": { 82 | "value": true 83 | } 84 | }, 85 | "resourceFilter": { 86 | "type": "ResourceContainsCondition" 87 | } 88 | }`), &cs)) 89 | 90 | require.Len(t, cs, 5) 91 | assert.IsType(t, &EqualsSubjectCondition{}, cs["owner"]) 92 | assert.IsType(t, &CIDRCondition{}, cs["clientIP"]) 93 | assert.IsType(t, &StringMatchCondition{}, cs["role"]) 94 | assert.IsType(t, &BooleanCondition{}, cs["hasElevatedPrivileges"]) 95 | assert.IsType(t, &ResourceContainsCondition{}, cs["resourceFilter"]) 96 | } 97 | 98 | func TestUnmarshalFails(t *testing.T) { 99 | cs := Conditions{} 100 | require.NotNil(t, json.Unmarshal([]byte(`{ 101 | "somefield": { 102 | "type": "DoesntExist" 103 | } 104 | }`), &cs)) 105 | } 106 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | // AllowAccess should be used as effect for policies that allow access. 24 | const AllowAccess = "allow" 25 | 26 | // DenyAccess should be used as effect for policies that deny access. 27 | const DenyAccess = "deny" 28 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | // Context is used as request's context. 24 | type Context map[string]interface{} 25 | -------------------------------------------------------------------------------- /docs/images/banner_ladon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ory/ladon/1d16bb356d68220899c40d8b4a81120af55a6482/docs/images/banner_ladon.png -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "net/http" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | var ( 30 | // ErrRequestDenied is returned when an access request can not be satisfied by any policy. 31 | ErrRequestDenied = &errorWithContext{ 32 | error: errors.New("Request was denied by default"), 33 | code: http.StatusForbidden, 34 | status: http.StatusText(http.StatusForbidden), 35 | reason: "The request was denied because no matching policy was found.", 36 | } 37 | 38 | // ErrRequestForcefullyDenied is returned when an access request is explicitly denied by a policy. 39 | ErrRequestForcefullyDenied = &errorWithContext{ 40 | error: errors.New("Request was forcefully denied"), 41 | code: http.StatusForbidden, 42 | status: http.StatusText(http.StatusForbidden), 43 | reason: "The request was denied because a policy denied request.", 44 | } 45 | 46 | // ErrNotFound is returned when a resource can not be found. 47 | ErrNotFound = &errorWithContext{ 48 | error: errors.New("Resource could not be found"), 49 | code: http.StatusNotFound, 50 | status: http.StatusText(http.StatusNotFound), 51 | } 52 | ) 53 | 54 | func NewErrResourceNotFound(err error) error { 55 | if err == nil { 56 | err = errors.New("not found") 57 | } 58 | 59 | return errors.WithStack(&errorWithContext{ 60 | error: err, 61 | code: http.StatusNotFound, 62 | status: http.StatusText(http.StatusNotFound), 63 | reason: "The requested resource could not be found.", 64 | }) 65 | } 66 | 67 | type errorWithContext struct { 68 | code int 69 | reason string 70 | status string 71 | error 72 | } 73 | 74 | // StatusCode returns the status code of this error. 75 | func (e *errorWithContext) StatusCode() int { 76 | return e.code 77 | } 78 | 79 | // RequestID returns the ID of the request that caused the error, if applicable. 80 | func (e *errorWithContext) RequestID() string { 81 | return "" 82 | } 83 | 84 | // Reason returns the reason for the error, if applicable. 85 | func (e *errorWithContext) Reason() string { 86 | return e.reason 87 | } 88 | 89 | // ID returns the error id, if applicable. 90 | func (e *errorWithContext) Status() string { 91 | return e.status 92 | } 93 | 94 | // Details returns details on the error, if applicable. 95 | func (e *errorWithContext) Details() []map[string]interface{} { 96 | return []map[string]interface{}{} 97 | } 98 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "errors" 25 | "testing" 26 | 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestNewErrResourceNotFound(t *testing.T) { 31 | assert.EqualError(t, NewErrResourceNotFound(errors.New("not found")), "not found") 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ory/ladon 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/dlclark/regexp2 v1.2.0 7 | github.com/golang/mock v1.6.0 8 | github.com/hashicorp/golang-lru v0.5.0 9 | github.com/ory/pagination v0.0.1 10 | github.com/pborman/uuid v1.2.0 11 | github.com/pkg/errors v0.8.0 12 | github.com/stretchr/testify v1.2.2 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/google/uuid v1.0.0 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= 4 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 5 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 6 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 7 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 8 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 10 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 11 | github.com/ory/pagination v0.0.1 h1:Zp+0n/UXSGYlJAMN0BuRjZhULsQRebGHfqByKtZXNYI= 12 | github.com/ory/pagination v0.0.1/go.mod h1:d1ToRROAUleriPhmb2dYbhANhhLwZ8s395m2yJCDFh8= 13 | github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= 14 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 15 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 16 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= 20 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 21 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 22 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 23 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 24 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 25 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 26 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 27 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 28 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 35 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 38 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 39 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 40 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 41 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 42 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 43 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | -------------------------------------------------------------------------------- /ladon.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "context" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | // Ladon is an implementation of Warden. 30 | type Ladon struct { 31 | Manager Manager 32 | Matcher matcher 33 | AuditLogger AuditLogger 34 | Metric Metric 35 | } 36 | 37 | func (l *Ladon) matcher() matcher { 38 | if l.Matcher != nil { 39 | return l.Matcher 40 | } 41 | return DefaultMatcher 42 | } 43 | 44 | func (l *Ladon) auditLogger() AuditLogger { 45 | if l.AuditLogger != nil { 46 | return l.AuditLogger 47 | } 48 | return DefaultAuditLogger 49 | } 50 | 51 | func (l *Ladon) metric() Metric { 52 | if l.Metric == nil { 53 | l.Metric = DefaultMetric 54 | } 55 | return l.Metric 56 | } 57 | 58 | // IsAllowed returns nil if subject s has permission p on resource r with context c or an error otherwise. 59 | func (l *Ladon) IsAllowed(ctx context.Context, r *Request) (err error) { 60 | policies, err := l.Manager.FindRequestCandidates(ctx, r) 61 | if err != nil { 62 | go l.metric().RequestProcessingError(*r, nil, err) 63 | return err 64 | } 65 | 66 | // Although the manager is responsible of matching the policies, it might decide to just scan for 67 | // subjects, it might return all policies, or it might have a different pattern matching than Golang. 68 | // Thus, we need to make sure that we actually matched the right policies. 69 | return l.DoPoliciesAllow(ctx, r, policies) 70 | } 71 | 72 | // DoPoliciesAllow returns nil if subject s has permission p on resource r with context c for a given policy list or an error otherwise. 73 | // The IsAllowed interface should be preferred since it uses the manager directly. This is a lower level interface for when you don't want to use the ladon manager. 74 | func (l *Ladon) DoPoliciesAllow(ctx context.Context, r *Request, policies []Policy) (err error) { 75 | var allowed = false 76 | var deciders = Policies{} 77 | 78 | // Iterate through all policies 79 | for _, p := range policies { 80 | 81 | // Does the action match with one of the policies? 82 | // This is the first check because usually actions are a superset of get|update|delete|set 83 | // and thus match faster. 84 | if pm, err := l.matcher().Matches(p, p.GetActions(), r.Action); err != nil { 85 | go l.metric().RequestProcessingError(*r, p, err) 86 | return errors.WithStack(err) 87 | } else if !pm { 88 | // no, continue to next policy 89 | continue 90 | } 91 | 92 | // Does the subject match with one of the policies? 93 | // There are usually less subjects than resources which is why this is checked 94 | // before checking for resources. 95 | if sm, err := l.matcher().Matches(p, p.GetSubjects(), r.Subject); err != nil { 96 | go l.metric().RequestProcessingError(*r, p, err) 97 | return err 98 | } else if !sm { 99 | // no, continue to next policy 100 | continue 101 | } 102 | 103 | // Does the resource match with one of the policies? 104 | if rm, err := l.matcher().Matches(p, p.GetResources(), r.Resource); err != nil { 105 | go l.metric().RequestProcessingError(*r, p, err) 106 | return errors.WithStack(err) 107 | } else if !rm { 108 | // no, continue to next policy 109 | continue 110 | } 111 | 112 | // Are the policies conditions met? 113 | // This is checked first because it usually has a small complexity. 114 | if !l.passesConditions(ctx, p, r) { 115 | // no, continue to next policy 116 | continue 117 | } 118 | 119 | // Is the policy's effect `deny`? If yes, this overrides all allow policies -> access denied. 120 | if !p.AllowAccess() { 121 | deciders = append(deciders, p) 122 | l.auditLogger().LogRejectedAccessRequest(ctx, r, policies, deciders) 123 | go l.metric().RequestDeniedBy(*r, p) 124 | return errors.WithStack(ErrRequestForcefullyDenied) 125 | } 126 | 127 | allowed = true 128 | deciders = append(deciders, p) 129 | } 130 | 131 | if !allowed { 132 | go l.metric().RequestNoMatch(*r) 133 | 134 | l.auditLogger().LogRejectedAccessRequest(ctx, r, policies, deciders) 135 | return errors.WithStack(ErrRequestDenied) 136 | } 137 | 138 | l.auditLogger().LogGrantedAccessRequest(ctx, r, policies, deciders) 139 | l.metric().RequestAllowedBy(*r, deciders) 140 | 141 | return nil 142 | } 143 | 144 | func (l *Ladon) passesConditions(ctx context.Context, p Policy, r *Request) bool { 145 | for key, condition := range p.GetConditions() { 146 | if pass := condition.Fulfills(ctx, r.Context[key], r); !pass { 147 | return false 148 | } 149 | } 150 | return true 151 | } 152 | -------------------------------------------------------------------------------- /ladon_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | 31 | . "github.com/ory/ladon" 32 | . "github.com/ory/ladon/manager/memory" 33 | ) 34 | 35 | // A bunch of exemplary policies 36 | var pols = []Policy{ 37 | &DefaultPolicy{ 38 | ID: "0", 39 | Description: `This policy allows max, peter, zac and ken to create, delete and get the listed resources, 40 | but only if the client ip matches and the request states that they are the owner of those resources as well.`, 41 | Subjects: []string{"max", "peter", ""}, 42 | Resources: []string{"myrn:some.domain.com:resource:123", "myrn:some.domain.com:resource:345", "myrn:something:foo:<.+>"}, 43 | Actions: []string{"", "get"}, 44 | Effect: AllowAccess, 45 | Conditions: Conditions{ 46 | "owner": &EqualsSubjectCondition{}, 47 | "clientIP": &CIDRCondition{ 48 | CIDR: "127.0.0.1/32", 49 | }, 50 | }, 51 | }, 52 | &DefaultPolicy{ 53 | ID: "1", 54 | Description: "This policy allows max to update any resource", 55 | Subjects: []string{"max"}, 56 | Actions: []string{"update"}, 57 | Resources: []string{"<.*>"}, 58 | Effect: AllowAccess, 59 | }, 60 | &DefaultPolicy{ 61 | ID: "3", 62 | Description: "This policy denies max to broadcast any of the resources", 63 | Subjects: []string{"max"}, 64 | Actions: []string{"broadcast"}, 65 | Resources: []string{"<.*>"}, 66 | Effect: DenyAccess, 67 | }, 68 | &DefaultPolicy{ 69 | ID: "2", 70 | Description: "This policy denies max to broadcast any of the resources", 71 | Subjects: []string{"max"}, 72 | Actions: []string{"random"}, 73 | Resources: []string{"<.*>"}, 74 | Effect: DenyAccess, 75 | }, 76 | &DefaultPolicy{ 77 | ID: "4", 78 | Description: "This policy allows swen to update any resource except `protected` resources", 79 | Subjects: []string{"swen"}, 80 | Actions: []string{"update"}, 81 | Resources: []string{"myrn:some.domain.com:resource:<(?!protected).*>"}, 82 | Effect: AllowAccess, 83 | }, 84 | &DefaultPolicy{ 85 | ID: "5", 86 | Description: "This policy allows richard to update resources which names consists of digits only", 87 | Subjects: []string{"richard"}, 88 | Actions: []string{"update"}, 89 | Resources: []string{"myrn:some.domain.com:resource:<[[:digit:]]+>"}, 90 | Effect: AllowAccess, 91 | }, 92 | } 93 | 94 | // Some test cases 95 | var cases = []struct { 96 | description string 97 | accessRequest *Request 98 | expectErr bool 99 | }{ 100 | { 101 | description: "should fail because no policy is matching as field clientIP does not satisfy the CIDR condition of policy 1.", 102 | accessRequest: &Request{ 103 | Subject: "peter", 104 | Action: "delete", 105 | Resource: "myrn:some.domain.com:resource:123", 106 | Context: Context{ 107 | "owner": "peter", 108 | "clientIP": "0.0.0.0", 109 | }, 110 | }, 111 | expectErr: true, 112 | }, 113 | { 114 | description: "should fail because no policy is matching as the owner of the resource 123 is zac, not peter!", 115 | accessRequest: &Request{ 116 | Subject: "peter", 117 | Action: "delete", 118 | Resource: "myrn:some.domain.com:resource:123", 119 | Context: Context{ 120 | "owner": "zac", 121 | "clientIP": "127.0.0.1", 122 | }, 123 | }, 124 | expectErr: true, 125 | }, 126 | { 127 | description: "should pass because policy 1 is matching and has effect allow.", 128 | accessRequest: &Request{ 129 | Subject: "peter", 130 | Action: "delete", 131 | Resource: "myrn:some.domain.com:resource:123", 132 | Context: Context{ 133 | "owner": "peter", 134 | "clientIP": "127.0.0.1", 135 | }, 136 | }, 137 | expectErr: false, 138 | }, 139 | { 140 | description: "should pass because max is allowed to update all resources.", 141 | accessRequest: &Request{ 142 | Subject: "max", 143 | Action: "update", 144 | Resource: "myrn:some.domain.com:resource:123", 145 | }, 146 | expectErr: false, 147 | }, 148 | { 149 | description: "should pass because max is allowed to update all resource, even if none is given.", 150 | accessRequest: &Request{ 151 | Subject: "max", 152 | Action: "update", 153 | Resource: "", 154 | }, 155 | expectErr: false, 156 | }, 157 | { 158 | description: "should fail because max is not allowed to broadcast any resource.", 159 | accessRequest: &Request{ 160 | Subject: "max", 161 | Action: "broadcast", 162 | Resource: "myrn:some.domain.com:resource:123", 163 | }, 164 | expectErr: true, 165 | }, 166 | { 167 | description: "should fail because max is not allowed to broadcast any resource, even empty ones!", 168 | accessRequest: &Request{ 169 | Subject: "max", 170 | Action: "broadcast", 171 | }, 172 | expectErr: true, 173 | }, 174 | { 175 | description: "should pass because swen is allowed to update all resources except `protected` resources.", 176 | accessRequest: &Request{ 177 | Subject: "swen", 178 | Action: "update", 179 | Resource: "myrn:some.domain.com:resource:123", 180 | }, 181 | expectErr: false, 182 | }, 183 | { 184 | description: "should fail because swen is not allowed to update `protected` resource", 185 | accessRequest: &Request{ 186 | Subject: "swen", 187 | Action: "update", 188 | Resource: "myrn:some.domain.com:resource:protected123", 189 | }, 190 | expectErr: true, 191 | }, 192 | { 193 | description: "should fail because richard is not allowed to update a resource with alphanumeric name", 194 | accessRequest: &Request{ 195 | Subject: "richard", 196 | Action: "update", 197 | Resource: "myrn:some.domain.com:resource:protected123", 198 | }, 199 | expectErr: true, 200 | }, 201 | { 202 | description: "should pass because richard is allowed to update a resources with a name containing digits only", 203 | accessRequest: &Request{ 204 | Subject: "richard", 205 | Action: "update", 206 | Resource: "myrn:some.domain.com:resource:25222", 207 | }, 208 | expectErr: false, 209 | }, 210 | } 211 | 212 | func TestLadon(t *testing.T) { 213 | 214 | ctx := context.Background() 215 | 216 | // Instantiate ladon with the default in-memory store. 217 | warden := &Ladon{Manager: NewMemoryManager()} 218 | 219 | // Add the policies defined above to the memory manager. 220 | for _, pol := range pols { 221 | require.Nil(t, warden.Manager.Create(ctx, pol)) 222 | } 223 | 224 | for i := 0; i < len(pols); i++ { 225 | polices, err := warden.Manager.GetAll(ctx, int64(1), int64(i)) 226 | require.NoError(t, err) 227 | p, err := warden.Manager.Get(ctx, fmt.Sprintf("%d", i)) 228 | if err == nil { 229 | AssertPolicyEqual(t, p, polices[0]) 230 | } 231 | } 232 | 233 | for k, c := range cases { 234 | t.Run(fmt.Sprintf("case=%d-%s", k, c.description), func(t *testing.T) { 235 | 236 | // This is where we ask the warden if the access requests should be granted 237 | err := warden.IsAllowed(ctx, c.accessRequest) 238 | 239 | assert.Equal(t, c.expectErr, err != nil) 240 | }) 241 | } 242 | 243 | } 244 | 245 | func TestLadonEmpty(t *testing.T) { 246 | 247 | ctx := context.Background() 248 | 249 | // If no policy was given, the warden must return an error! 250 | warden := &Ladon{Manager: NewMemoryManager()} 251 | assert.NotNil(t, warden.IsAllowed(ctx, &Request{})) 252 | } 253 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ory/ladon/1d16bb356d68220899c40d8b4a81120af55a6482/logo.png -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // Manager is responsible for managing and persisting policies. 26 | type Manager interface { 27 | 28 | // Create persists the policy. 29 | Create(ctx context.Context, policy Policy) error 30 | 31 | // Update updates an existing policy. 32 | Update(ctx context.Context, policy Policy) error 33 | 34 | // Get retrieves a policy. 35 | Get(ctx context.Context, id string) (Policy, error) 36 | 37 | // Delete removes a policy. 38 | Delete(ctx context.Context, id string) error 39 | 40 | // GetAll retrieves all policies. 41 | GetAll(ctx context.Context, limit, offset int64) (Policies, error) 42 | 43 | // FindRequestCandidates returns candidates that could match the request object. It either returns 44 | // a set that exactly matches the request, or a superset of it. If an error occurs, it returns nil and 45 | // the error. 46 | FindRequestCandidates(ctx context.Context, r *Request) (Policies, error) 47 | 48 | // FindPoliciesForSubject returns policies that could match the subject. It either returns 49 | // a set of policies that applies to the subject, or a superset of it. 50 | // If an error occurs, it returns nil and the error. 51 | FindPoliciesForSubject(ctx context.Context, subject string) (Policies, error) 52 | 53 | // FindPoliciesForResource returns policies that could match the resource. It either returns 54 | // a set of policies that apply to the resource, or a superset of it. 55 | // If an error occurs, it returns nil and the error. 56 | FindPoliciesForResource(ctx context.Context, resource string) (Policies, error) 57 | } 58 | -------------------------------------------------------------------------------- /manager/memory/manager_memory.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package memory 22 | 23 | import ( 24 | "context" 25 | "sort" 26 | "sync" 27 | 28 | "github.com/pkg/errors" 29 | 30 | . "github.com/ory/ladon" 31 | "github.com/ory/pagination" 32 | ) 33 | 34 | // MemoryManager is an in-memory (non-persistent) implementation of Manager. 35 | type MemoryManager struct { 36 | Policies map[string]Policy 37 | sync.RWMutex 38 | } 39 | 40 | // NewMemoryManager constructs and initializes new MemoryManager with no policies. 41 | func NewMemoryManager() *MemoryManager { 42 | return &MemoryManager{ 43 | Policies: map[string]Policy{}, 44 | } 45 | } 46 | 47 | // Update updates an existing policy. 48 | func (m *MemoryManager) Update(ctx context.Context, policy Policy) error { 49 | m.Lock() 50 | defer m.Unlock() 51 | m.Policies[policy.GetID()] = policy 52 | return nil 53 | } 54 | 55 | // GetAll returns all policies. 56 | func (m *MemoryManager) GetAll(ctx context.Context, limit, offset int64) (Policies, error) { 57 | keys := make([]string, len(m.Policies)) 58 | i := 0 59 | m.RLock() 60 | for key := range m.Policies { 61 | keys[i] = key 62 | i++ 63 | } 64 | 65 | start, end := pagination.Index(int(limit), int(offset), len(m.Policies)) 66 | sort.Strings(keys) 67 | ps := make(Policies, len(keys[start:end])) 68 | i = 0 69 | for _, key := range keys[start:end] { 70 | ps[i] = m.Policies[key] 71 | i++ 72 | } 73 | m.RUnlock() 74 | return ps, nil 75 | } 76 | 77 | // Create a new pollicy to MemoryManager. 78 | func (m *MemoryManager) Create(ctx context.Context, policy Policy) error { 79 | m.Lock() 80 | defer m.Unlock() 81 | 82 | if _, found := m.Policies[policy.GetID()]; found { 83 | return errors.New("Policy exists") 84 | } 85 | 86 | m.Policies[policy.GetID()] = policy 87 | return nil 88 | } 89 | 90 | // Get retrieves a policy. 91 | func (m *MemoryManager) Get(ctx context.Context, id string) (Policy, error) { 92 | m.RLock() 93 | defer m.RUnlock() 94 | p, ok := m.Policies[id] 95 | if !ok { 96 | return nil, errors.New("Not found") 97 | } 98 | 99 | return p, nil 100 | } 101 | 102 | // Delete removes a policy. 103 | func (m *MemoryManager) Delete(ctx context.Context, id string) error { 104 | m.Lock() 105 | defer m.Unlock() 106 | delete(m.Policies, id) 107 | return nil 108 | } 109 | 110 | func (m *MemoryManager) findAllPolicies() (Policies, error) { 111 | m.RLock() 112 | defer m.RUnlock() 113 | ps := make(Policies, len(m.Policies)) 114 | var count int 115 | for _, p := range m.Policies { 116 | ps[count] = p 117 | count++ 118 | } 119 | return ps, nil 120 | } 121 | 122 | // FindRequestCandidates returns candidates that could match the request object. It either returns 123 | // a set that exactly matches the request, or a superset of it. If an error occurs, it returns nil and 124 | // the error. 125 | func (m *MemoryManager) FindRequestCandidates(ctx context.Context, r *Request) (Policies, error) { 126 | return m.findAllPolicies() 127 | } 128 | 129 | // FindPoliciesForSubject returns policies that could match the subject. It either returns 130 | // a set of policies that applies to the subject, or a superset of it. 131 | // If an error occurs, it returns nil and the error. 132 | func (m *MemoryManager) FindPoliciesForSubject(ctx context.Context, subject string) (Policies, error) { 133 | return m.findAllPolicies() 134 | } 135 | 136 | // FindPoliciesForResource returns policies that could match the resource. It either returns 137 | // a set of policies that apply to the resource, or a superset of it. 138 | // If an error occurs, it returns nil and the error. 139 | func (m *MemoryManager) FindPoliciesForResource(ctx context.Context, resource string) (Policies, error) { 140 | return m.findAllPolicies() 141 | } 142 | -------------------------------------------------------------------------------- /manager_all_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "fmt" 25 | "testing" 26 | 27 | . "github.com/ory/ladon" 28 | . "github.com/ory/ladon/manager/memory" 29 | ) 30 | 31 | var managers = map[string]Manager{} 32 | 33 | func TestMain(m *testing.M) { 34 | connectMEM() 35 | } 36 | 37 | func connectMEM() { 38 | managers["memory"] = NewMemoryManager() 39 | } 40 | 41 | func TestManagers(t *testing.T) { 42 | t.Run("type=get errors", func(t *testing.T) { 43 | for k, s := range managers { 44 | t.Run("manager="+k, HelperTestGetErrors(s)) 45 | } 46 | }) 47 | 48 | t.Run("type=CRUD", func(t *testing.T) { 49 | for k, s := range managers { 50 | t.Run(fmt.Sprintf("manager=%s", k), HelperTestCreateGetDelete(s)) 51 | } 52 | }) 53 | 54 | t.Run("type=find", func(t *testing.T) { 55 | for k, s := range map[string]Manager{ 56 | "postgres": managers["postgres"], 57 | "mysql": managers["mysql"], 58 | } { 59 | t.Run(fmt.Sprintf("manager=%s", k), HelperTestFindPoliciesForSubject(k, s)) 60 | t.Run(fmt.Sprintf("manager=%s", k), HelperTestFindPoliciesForResource(k, s)) 61 | } 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /manager_helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "reflect" 27 | "testing" 28 | 29 | "github.com/ory/ladon" 30 | "github.com/pborman/uuid" 31 | "github.com/pkg/errors" 32 | "github.com/stretchr/testify/assert" 33 | "github.com/stretchr/testify/require" 34 | ) 35 | 36 | var TestManagerPolicies = []*ladon.DefaultPolicy{ 37 | { 38 | ID: uuid.New(), 39 | Description: "description", 40 | Subjects: []string{"user", "anonymous"}, 41 | Effect: ladon.AllowAccess, 42 | Resources: []string{"article", "user"}, 43 | Actions: []string{"create", "update"}, 44 | Conditions: ladon.Conditions{}, 45 | }, 46 | { 47 | ID: uuid.New(), 48 | Description: "description", 49 | Subjects: []string{}, 50 | Effect: ladon.AllowAccess, 51 | Resources: []string{""}, 52 | Actions: []string{"view"}, 53 | Conditions: ladon.Conditions{}, 54 | }, 55 | { 56 | ID: uuid.New(), 57 | Description: "description", 58 | Subjects: []string{}, 59 | Effect: ladon.AllowAccess, 60 | Resources: []string{}, 61 | Actions: []string{"view"}, 62 | Conditions: ladon.Conditions{}, 63 | }, 64 | { 65 | ID: uuid.New(), 66 | Description: "description", 67 | Subjects: []string{}, 68 | Effect: ladon.AllowAccess, 69 | Resources: []string{}, 70 | Actions: []string{}, 71 | Conditions: ladon.Conditions{}, 72 | }, 73 | { 74 | ID: uuid.New(), 75 | Description: "description", 76 | Subjects: []string{}, 77 | Effect: ladon.AllowAccess, 78 | Resources: []string{"foo"}, 79 | Actions: []string{}, 80 | Conditions: ladon.Conditions{}, 81 | }, 82 | { 83 | ID: uuid.New(), 84 | Description: "description", 85 | Subjects: []string{"foo"}, 86 | Effect: ladon.AllowAccess, 87 | Resources: []string{"foo"}, 88 | Actions: []string{}, 89 | Conditions: ladon.Conditions{}, 90 | }, 91 | { 92 | ID: uuid.New(), 93 | Description: "description", 94 | Subjects: []string{"foo"}, 95 | Effect: ladon.AllowAccess, 96 | Resources: []string{}, 97 | Actions: []string{}, 98 | Conditions: ladon.Conditions{}, 99 | }, 100 | { 101 | ID: uuid.New(), 102 | Description: "description", 103 | Effect: ladon.AllowAccess, 104 | Conditions: ladon.Conditions{}, 105 | }, 106 | { 107 | ID: uuid.New(), 108 | Description: "description", 109 | Subjects: []string{""}, 110 | Effect: ladon.DenyAccess, 111 | Resources: []string{"article", "user"}, 112 | Actions: []string{"view"}, 113 | Conditions: ladon.Conditions{ 114 | "owner": &ladon.EqualsSubjectCondition{}, 115 | }, 116 | }, 117 | { 118 | ID: uuid.New(), 119 | Description: "description", 120 | Subjects: []string{"", "peter"}, 121 | Effect: ladon.DenyAccess, 122 | Resources: []string{".*"}, 123 | Actions: []string{"disable"}, 124 | Conditions: ladon.Conditions{ 125 | "ip": &ladon.CIDRCondition{ 126 | CIDR: "1234", 127 | }, 128 | "owner": &ladon.EqualsSubjectCondition{}, 129 | }, 130 | }, 131 | { 132 | ID: uuid.New(), 133 | Description: "description", 134 | Subjects: []string{"<.*>"}, 135 | Effect: ladon.AllowAccess, 136 | Resources: []string{""}, 137 | Actions: []string{"view"}, 138 | Conditions: ladon.Conditions{ 139 | "ip": &ladon.CIDRCondition{ 140 | CIDR: "1234", 141 | }, 142 | "owner": &ladon.EqualsSubjectCondition{}, 143 | }, 144 | }, 145 | { 146 | ID: uuid.New(), 147 | Description: "description", 148 | Subjects: []string{""}, 149 | Effect: ladon.AllowAccess, 150 | Resources: []string{""}, 151 | Actions: []string{"view"}, 152 | Conditions: ladon.Conditions{ 153 | "ip": &ladon.CIDRCondition{ 154 | CIDR: "1234", 155 | }, 156 | "owner": &ladon.EqualsSubjectCondition{}, 157 | }, 158 | }, 159 | //Two new policies which do not persist in MySQL correctly 160 | { 161 | ID: uuid.New(), 162 | Description: "A failed policy", 163 | Subjects: []string{"supplier"}, 164 | Effect: ladon.AllowAccess, 165 | Resources: []string{"product:<.*>"}, 166 | Actions: []string{"update"}, 167 | Conditions: ladon.Conditions{}, 168 | }, 169 | { 170 | ID: uuid.New(), 171 | Description: "Another failed policy", 172 | Subjects: []string{"buyer"}, 173 | Effect: ladon.AllowAccess, 174 | Resources: []string{"products:attributeGroup:<.*>"}, 175 | Actions: []string{"create"}, 176 | Conditions: ladon.Conditions{}, 177 | }, 178 | } 179 | 180 | var testPolicies = []*ladon.DefaultPolicy{ 181 | { 182 | ID: uuid.New(), 183 | Description: "description", 184 | Subjects: []string{"sql<.*>match"}, 185 | Effect: ladon.AllowAccess, 186 | Resources: []string{"master", "user", "article"}, 187 | Actions: []string{"create", "update", "delete"}, 188 | Conditions: ladon.Conditions{ 189 | "foo": &ladon.StringEqualCondition{ 190 | Equals: "foo", 191 | }, 192 | }, 193 | }, 194 | { 195 | ID: uuid.New(), 196 | Description: "description", 197 | Subjects: []string{"sqlmatch"}, 198 | Effect: ladon.AllowAccess, 199 | Resources: []string{"master", "user", "article"}, 200 | Actions: []string{"create", "update", "delete"}, 201 | Conditions: ladon.Conditions{ 202 | "foo": &ladon.StringEqualCondition{ 203 | Equals: "foo", 204 | }, 205 | }, 206 | }, 207 | { 208 | ID: uuid.New(), 209 | Description: "description", 210 | Subjects: []string{}, 211 | Effect: ladon.AllowAccess, 212 | Resources: []string{"master", "user", "article"}, 213 | Actions: []string{"create", "update", "delete"}, 214 | Conditions: ladon.Conditions{ 215 | "foo": &ladon.StringEqualCondition{ 216 | Equals: "foo", 217 | }, 218 | }, 219 | }, 220 | { 221 | ID: uuid.New(), 222 | Description: "description", 223 | Effect: ladon.AllowAccess, 224 | Resources: []string{"master", "user", "article"}, 225 | Actions: []string{"create", "update", "delete"}, 226 | Conditions: ladon.Conditions{ 227 | "foo": &ladon.StringEqualCondition{ 228 | Equals: "foo", 229 | }, 230 | }, 231 | }, 232 | { 233 | ID: uuid.New(), 234 | Description: "description", 235 | Subjects: []string{"some"}, 236 | Effect: ladon.AllowAccess, 237 | Resources: []string{"sqlmatch_resource"}, 238 | Actions: []string{"create", "update", "delete"}, 239 | Conditions: ladon.Conditions{ 240 | "foo": &ladon.StringEqualCondition{ 241 | Equals: "foo", 242 | }, 243 | }, 244 | }, 245 | { 246 | ID: uuid.New(), 247 | Description: "description", 248 | Subjects: []string{"other"}, 249 | Effect: ladon.AllowAccess, 250 | Resources: []string{"sql<.*>resource"}, 251 | Actions: []string{"create", "update", "delete"}, 252 | Conditions: ladon.Conditions{ 253 | "foo": &ladon.StringEqualCondition{ 254 | Equals: "foo", 255 | }, 256 | }, 257 | }, 258 | } 259 | 260 | func HelperTestFindPoliciesForSubject(k string, s ladon.Manager) func(t *testing.T) { 261 | ctx := context.Background() 262 | 263 | return func(t *testing.T) { 264 | for _, c := range testPolicies { 265 | t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { 266 | require.NoError(t, s.Create(ctx, c)) 267 | }) 268 | } 269 | 270 | res, err := s.FindRequestCandidates(ctx, &ladon.Request{ 271 | Subject: "sqlmatch", 272 | Resource: "article", 273 | Action: "create", 274 | }) 275 | require.NoError(t, err) 276 | require.Len(t, res, 2) 277 | 278 | if testPolicies[0].ID == res[0].GetID() { 279 | AssertPolicyEqual(t, testPolicies[0], res[0]) 280 | AssertPolicyEqual(t, testPolicies[1], res[1]) 281 | } else { 282 | AssertPolicyEqual(t, testPolicies[0], res[1]) 283 | AssertPolicyEqual(t, testPolicies[1], res[0]) 284 | } 285 | 286 | res, err = s.FindRequestCandidates(ctx, &ladon.Request{ 287 | Subject: "sqlamatch", 288 | Resource: "article", 289 | Action: "create", 290 | }) 291 | 292 | require.NoError(t, err) 293 | require.Len(t, res, 1) 294 | AssertPolicyEqual(t, testPolicies[0], res[0]) 295 | } 296 | } 297 | 298 | func HelperTestFindPoliciesForResource(k string, s ladon.Manager) func(t *testing.T) { 299 | ctx := context.Background() 300 | 301 | return func(t *testing.T) { 302 | for _, c := range testPolicies { 303 | t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { 304 | require.NoError(t, s.Create(ctx, c)) 305 | }) 306 | } 307 | 308 | res, err := s.FindPoliciesForResource(ctx, "sqlmatch_resource") 309 | require.NoError(t, err) 310 | require.Len(t, res, 2) 311 | 312 | if testPolicies[len(testPolicies)-2].ID == res[0].GetID() { 313 | AssertPolicyEqual(t, testPolicies[len(testPolicies)-2], res[0]) 314 | AssertPolicyEqual(t, testPolicies[len(testPolicies)-1], res[1]) 315 | } else { 316 | AssertPolicyEqual(t, testPolicies[len(testPolicies)-2], res[1]) 317 | AssertPolicyEqual(t, testPolicies[len(testPolicies)-1], res[0]) 318 | } 319 | 320 | res, err = s.FindPoliciesForResource(ctx, "sqlamatch_resource") 321 | 322 | require.NoError(t, err) 323 | require.Len(t, res, 1) 324 | AssertPolicyEqual(t, testPolicies[len(testPolicies)-1], res[0]) 325 | } 326 | } 327 | 328 | func AssertPolicyEqual(t *testing.T, expected, got ladon.Policy) { 329 | assert.Equal(t, expected.GetID(), got.GetID()) 330 | assert.Equal(t, expected.GetDescription(), got.GetDescription()) 331 | assert.Equal(t, expected.GetEffect(), got.GetEffect()) 332 | 333 | // This won't work in the memory manager 334 | //assert.NotNil(t, got.GetActions()) 335 | //assert.NotNil(t, got.GetResources()) 336 | //assert.NotNil(t, got.GetSubjects()) 337 | 338 | assert.NoError(t, testEq(expected.GetActions(), got.GetActions())) 339 | assert.NoError(t, testEq(expected.GetResources(), got.GetResources())) 340 | assert.NoError(t, testEq(expected.GetSubjects(), got.GetSubjects())) 341 | assert.EqualValues(t, expected.GetConditions(), got.GetConditions()) 342 | } 343 | 344 | func testEq(a, b []string) error { 345 | // We don't care about nil types 346 | //if a == nil && b == nil { 347 | // return true 348 | //} 349 | // 350 | //if a == nil || b == nil { 351 | // return false 352 | //} 353 | 354 | if len(a) != len(b) { 355 | return errors.Errorf("Length not equal: %v (%d) != %v (%d)", a, len(a), b, len(b)) 356 | } 357 | 358 | var found bool 359 | for i := range a { 360 | found = false 361 | 362 | for y := range b { 363 | if a[i] == b[y] { 364 | found = true 365 | break 366 | } 367 | } 368 | 369 | if !found { 370 | return errors.Errorf("No match found: %d from %v in %v", i, a, b) 371 | } 372 | } 373 | 374 | return nil 375 | } 376 | 377 | func HelperTestGetErrors(s ladon.Manager) func(t *testing.T) { 378 | ctx := context.Background() 379 | 380 | return func(t *testing.T) { 381 | _, err := s.Get(ctx, uuid.New()) 382 | assert.Error(t, err) 383 | 384 | _, err = s.Get(ctx, "asdf") 385 | assert.Error(t, err) 386 | } 387 | } 388 | 389 | func HelperTestCreateGetDelete(s ladon.Manager) func(t *testing.T) { 390 | ctx := context.Background() 391 | 392 | return func(t *testing.T) { 393 | for i, c := range TestManagerPolicies { 394 | t.Run(fmt.Sprintf("case=%d/id=%s/type=create", i, c.GetID()), func(t *testing.T) { 395 | _, err := s.Get(ctx, c.GetID()) 396 | require.Error(t, err) 397 | require.NoError(t, s.Create(ctx, c)) 398 | }) 399 | 400 | t.Run(fmt.Sprintf("case=%d/id=%s/type=query", i, c.GetID()), func(t *testing.T) { 401 | get, err := s.Get(ctx, c.GetID()) 402 | require.NoError(t, err) 403 | 404 | AssertPolicyEqual(t, c, get) 405 | }) 406 | 407 | t.Run(fmt.Sprintf("case=%d/id=%s/type=update", i, c.GetID()), func(t *testing.T) { 408 | c.Description = c.Description + "_updated" 409 | require.NoError(t, s.Update(ctx, c)) 410 | 411 | get, err := s.Get(ctx, c.GetID()) 412 | require.NoError(t, err) 413 | 414 | AssertPolicyEqual(t, c, get) 415 | }) 416 | 417 | t.Run(fmt.Sprintf("case=%d/id=%s/type=query", i, c.GetID()), func(t *testing.T) { 418 | get, err := s.Get(ctx, c.GetID()) 419 | require.NoError(t, err) 420 | 421 | AssertPolicyEqual(t, c, get) 422 | }) 423 | } 424 | 425 | t.Run("type=query-all", func(t *testing.T) { 426 | count := int64(len(TestManagerPolicies)) 427 | 428 | pols, err := s.GetAll(ctx, 100, 0) 429 | require.NoError(t, err) 430 | assert.Len(t, pols, len(TestManagerPolicies)) 431 | 432 | pols4, err := s.GetAll(ctx, 1, 0) 433 | require.NoError(t, err) 434 | assert.Len(t, pols4, 1) 435 | 436 | pols2, err := s.GetAll(ctx, 100, count-1) 437 | require.NoError(t, err) 438 | assert.Len(t, pols2, 1) 439 | 440 | pols3, err := s.GetAll(ctx, 100, count) 441 | require.NoError(t, err) 442 | assert.Len(t, pols3, 0) 443 | 444 | found := map[string]int{} 445 | for _, got := range pols { 446 | for _, expect := range TestManagerPolicies { 447 | if got.GetID() == expect.GetID() { 448 | assert.ObjectsAreEqualValues(t, reflect.DeepEqual(expect, got)) 449 | found[got.GetID()]++ 450 | } 451 | } 452 | } 453 | // for _, got := range pols { 454 | // for _, expect := range TestManagerPolicies { 455 | // //This is a modified equality check 456 | // if got.GetID() == expect.GetID() && reflect.DeepEqual(got.GetResources(), expect.GetResources()) && reflect.DeepEqual(got.GetActions(), expect.GetActions()) { 457 | // found[got.GetID()]++ 458 | // } 459 | // } 460 | // } 461 | assert.Len(t, found, len(TestManagerPolicies)) 462 | 463 | for _, f := range found { 464 | //This assert is supposed to pass 465 | assert.Equal(t, 1, f) 466 | } 467 | }) 468 | 469 | for i, c := range TestManagerPolicies { 470 | t.Run(fmt.Sprintf("case=%d/id=%s/type=delete", i, c.GetID()), func(t *testing.T) { 471 | assert.NoError(t, s.Delete(ctx, c.ID)) 472 | 473 | _, err := s.Get(ctx, c.GetID()) 474 | assert.Error(t, err) 475 | }) 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /manager_migrator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | type ManagerMigrator interface { 24 | Create(policy Policy) (err error) 25 | Migrate() (err error) 26 | GetManager() Manager 27 | } 28 | -------------------------------------------------------------------------------- /manager_mock_test.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/ory/ladon (interfaces: Manager) 3 | 4 | // Package ladon_test is a generated GoMock package. 5 | package ladon_test 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ladon "github.com/ory/ladon" 13 | ) 14 | 15 | // MockManager is a mock of Manager interface. 16 | type MockManager struct { 17 | ctrl *gomock.Controller 18 | recorder *MockManagerMockRecorder 19 | } 20 | 21 | // MockManagerMockRecorder is the mock recorder for MockManager. 22 | type MockManagerMockRecorder struct { 23 | mock *MockManager 24 | } 25 | 26 | // NewMockManager creates a new mock instance. 27 | func NewMockManager(ctrl *gomock.Controller) *MockManager { 28 | mock := &MockManager{ctrl: ctrl} 29 | mock.recorder = &MockManagerMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockManager) EXPECT() *MockManagerMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Create mocks base method. 39 | func (m *MockManager) Create(arg0 context.Context, arg1 ladon.Policy) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Create", arg0, arg1) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Create indicates an expected call of Create. 47 | func (mr *MockManagerMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), arg0, arg1) 50 | } 51 | 52 | // Delete mocks base method. 53 | func (m *MockManager) Delete(arg0 context.Context, arg1 string) error { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Delete", arg0, arg1) 56 | ret0, _ := ret[0].(error) 57 | return ret0 58 | } 59 | 60 | // Delete indicates an expected call of Delete. 61 | func (mr *MockManagerMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockManager)(nil).Delete), arg0, arg1) 64 | } 65 | 66 | // FindPoliciesForResource mocks base method. 67 | func (m *MockManager) FindPoliciesForResource(arg0 context.Context, arg1 string) (ladon.Policies, error) { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "FindPoliciesForResource", arg0, arg1) 70 | ret0, _ := ret[0].(ladon.Policies) 71 | ret1, _ := ret[1].(error) 72 | return ret0, ret1 73 | } 74 | 75 | // FindPoliciesForResource indicates an expected call of FindPoliciesForResource. 76 | func (mr *MockManagerMockRecorder) FindPoliciesForResource(arg0, arg1 interface{}) *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPoliciesForResource", reflect.TypeOf((*MockManager)(nil).FindPoliciesForResource), arg0, arg1) 79 | } 80 | 81 | // FindPoliciesForSubject mocks base method. 82 | func (m *MockManager) FindPoliciesForSubject(arg0 context.Context, arg1 string) (ladon.Policies, error) { 83 | m.ctrl.T.Helper() 84 | ret := m.ctrl.Call(m, "FindPoliciesForSubject", arg0, arg1) 85 | ret0, _ := ret[0].(ladon.Policies) 86 | ret1, _ := ret[1].(error) 87 | return ret0, ret1 88 | } 89 | 90 | // FindPoliciesForSubject indicates an expected call of FindPoliciesForSubject. 91 | func (mr *MockManagerMockRecorder) FindPoliciesForSubject(arg0, arg1 interface{}) *gomock.Call { 92 | mr.mock.ctrl.T.Helper() 93 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPoliciesForSubject", reflect.TypeOf((*MockManager)(nil).FindPoliciesForSubject), arg0, arg1) 94 | } 95 | 96 | // FindRequestCandidates mocks base method. 97 | func (m *MockManager) FindRequestCandidates(arg0 context.Context, arg1 *ladon.Request) (ladon.Policies, error) { 98 | m.ctrl.T.Helper() 99 | ret := m.ctrl.Call(m, "FindRequestCandidates", arg0, arg1) 100 | ret0, _ := ret[0].(ladon.Policies) 101 | ret1, _ := ret[1].(error) 102 | return ret0, ret1 103 | } 104 | 105 | // FindRequestCandidates indicates an expected call of FindRequestCandidates. 106 | func (mr *MockManagerMockRecorder) FindRequestCandidates(arg0, arg1 interface{}) *gomock.Call { 107 | mr.mock.ctrl.T.Helper() 108 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRequestCandidates", reflect.TypeOf((*MockManager)(nil).FindRequestCandidates), arg0, arg1) 109 | } 110 | 111 | // Get mocks base method. 112 | func (m *MockManager) Get(arg0 context.Context, arg1 string) (ladon.Policy, error) { 113 | m.ctrl.T.Helper() 114 | ret := m.ctrl.Call(m, "Get", arg0, arg1) 115 | ret0, _ := ret[0].(ladon.Policy) 116 | ret1, _ := ret[1].(error) 117 | return ret0, ret1 118 | } 119 | 120 | // Get indicates an expected call of Get. 121 | func (mr *MockManagerMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { 122 | mr.mock.ctrl.T.Helper() 123 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockManager)(nil).Get), arg0, arg1) 124 | } 125 | 126 | // GetAll mocks base method. 127 | func (m *MockManager) GetAll(arg0 context.Context, arg1, arg2 int64) (ladon.Policies, error) { 128 | m.ctrl.T.Helper() 129 | ret := m.ctrl.Call(m, "GetAll", arg0, arg1, arg2) 130 | ret0, _ := ret[0].(ladon.Policies) 131 | ret1, _ := ret[1].(error) 132 | return ret0, ret1 133 | } 134 | 135 | // GetAll indicates an expected call of GetAll. 136 | func (mr *MockManagerMockRecorder) GetAll(arg0, arg1, arg2 interface{}) *gomock.Call { 137 | mr.mock.ctrl.T.Helper() 138 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockManager)(nil).GetAll), arg0, arg1, arg2) 139 | } 140 | 141 | // Update mocks base method. 142 | func (m *MockManager) Update(arg0 context.Context, arg1 ladon.Policy) error { 143 | m.ctrl.T.Helper() 144 | ret := m.ctrl.Call(m, "Update", arg0, arg1) 145 | ret0, _ := ret[0].(error) 146 | return ret0 147 | } 148 | 149 | // Update indicates an expected call of Update. 150 | func (mr *MockManagerMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { 151 | mr.mock.ctrl.T.Helper() 152 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockManager)(nil).Update), arg0, arg1) 153 | } 154 | -------------------------------------------------------------------------------- /matcher.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | type matcher interface { 24 | Matches(p Policy, haystack []string, needle string) (matches bool, error error) 25 | } 26 | 27 | var DefaultMatcher = NewRegexpMatcher(512) 28 | -------------------------------------------------------------------------------- /matcher_regexp.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "strings" 25 | 26 | "github.com/dlclark/regexp2" 27 | "github.com/hashicorp/golang-lru" 28 | "github.com/pkg/errors" 29 | 30 | "github.com/ory/ladon/compiler" 31 | ) 32 | 33 | func NewRegexpMatcher(size int) *RegexpMatcher { 34 | if size <= 0 { 35 | size = 512 36 | } 37 | 38 | // golang-lru only returns an error if the cache's size is 0. This, we can safely ignore this error. 39 | cache, _ := lru.New(size) 40 | return &RegexpMatcher{ 41 | Cache: cache, 42 | } 43 | } 44 | 45 | type RegexpMatcher struct { 46 | *lru.Cache 47 | } 48 | 49 | func (m *RegexpMatcher) get(pattern string) *regexp2.Regexp { 50 | if val, ok := m.Cache.Get(pattern); !ok { 51 | return nil 52 | } else if reg, ok := val.(*regexp2.Regexp); !ok { 53 | return nil 54 | } else { 55 | return reg 56 | } 57 | } 58 | 59 | func (m *RegexpMatcher) set(pattern string, reg *regexp2.Regexp) { 60 | m.Cache.Add(pattern, reg) 61 | } 62 | 63 | // Matches a needle with an array of regular expressions and returns true if a match was found. 64 | func (m *RegexpMatcher) Matches(p Policy, haystack []string, needle string) (bool, error) { 65 | var reg *regexp2.Regexp 66 | var err error 67 | for _, h := range haystack { 68 | 69 | // This means that the current haystack item does not contain a regular expression 70 | if strings.Count(h, string(p.GetStartDelimiter())) == 0 { 71 | // If we have a simple string match, we've got a match! 72 | if h == needle { 73 | return true, nil 74 | } 75 | 76 | // Not string match, but also no regexp, continue with next haystack item 77 | continue 78 | } 79 | 80 | if reg = m.get(h); reg != nil { 81 | if matched, err := reg.MatchString(needle); err != nil { 82 | // according to regexp2 documentation: https://github.com/dlclark/regexp2#usage 83 | // The only error that the *Match* methods should return is a Timeout if you set the 84 | // re.MatchTimeout field. Any other error is a bug in the regexp2 package. 85 | return false, errors.WithStack(err) 86 | } else if matched { 87 | return true, nil 88 | } 89 | continue 90 | } 91 | 92 | reg, err = compiler.CompileRegex(h, p.GetStartDelimiter(), p.GetEndDelimiter()) 93 | if err != nil { 94 | return false, errors.WithStack(err) 95 | } 96 | 97 | m.set(h, reg) 98 | if matched, err := reg.MatchString(needle); err != nil { 99 | // according to regexp2 documentation: https://github.com/dlclark/regexp2#usage 100 | // The only error that the *Match* methods should return is a Timeout if you set the 101 | // re.MatchTimeout field. Any other error is a bug in the regexp2 package. 102 | return false, errors.WithStack(err) 103 | } else if matched { 104 | return true, nil 105 | } 106 | } 107 | return false, nil 108 | } 109 | -------------------------------------------------------------------------------- /metric.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | // Metric is used to expose metrics about authz 24 | type Metric interface { 25 | // RequestDeniedBy is called when we get explicit deny by policy 26 | RequestDeniedBy(Request, Policy) 27 | // RequestAllowedBy is called when a matching policy has been found. 28 | RequestAllowedBy(Request, Policies) 29 | // RequestNoMatch is called when no policy has matched our request 30 | RequestNoMatch(Request) 31 | // RequestProcessingError is called when unexpected error occured 32 | RequestProcessingError(Request, Policy, error) 33 | } 34 | -------------------------------------------------------------------------------- /metric_noop.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | // MetricNoOp is the default metrics implementation , that tracks nothing. 24 | type MetricNoOp struct{} 25 | 26 | func (*MetricNoOp) RequestDeniedBy(r Request, p Policy) {} 27 | func (*MetricNoOp) RequestAllowedBy(r Request, p Policies) {} 28 | func (*MetricNoOp) RequestNoMatch(r Request) {} 29 | func (*MetricNoOp) RequestProcessingError(r Request, p Policy, err error) {} 30 | 31 | var DefaultMetric = &MetricNoOp{} 32 | -------------------------------------------------------------------------------- /policy.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import ( 24 | "encoding/json" 25 | 26 | "github.com/pkg/errors" 27 | ) 28 | 29 | // Policies is an array of policies. 30 | type Policies []Policy 31 | 32 | // Policy represent a policy model. 33 | type Policy interface { 34 | // GetID returns the policies id. 35 | GetID() string 36 | 37 | // GetDescription returns the policies description. 38 | GetDescription() string 39 | 40 | // GetSubjects returns the policies subjects. 41 | GetSubjects() []string 42 | 43 | // AllowAccess returns true if the policy effect is allow, otherwise false. 44 | AllowAccess() bool 45 | 46 | // GetEffect returns the policies effect which might be 'allow' or 'deny'. 47 | GetEffect() string 48 | 49 | // GetResources returns the policies resources. 50 | GetResources() []string 51 | 52 | // GetActions returns the policies actions. 53 | GetActions() []string 54 | 55 | // GetConditions returns the policies conditions. 56 | GetConditions() Conditions 57 | 58 | // GetMeta returns the policies arbitrary metadata set by the user. 59 | GetMeta() []byte 60 | 61 | // GetStartDelimiter returns the delimiter which identifies the beginning of a regular expression. 62 | GetStartDelimiter() byte 63 | 64 | // GetEndDelimiter returns the delimiter which identifies the end of a regular expression. 65 | GetEndDelimiter() byte 66 | } 67 | 68 | // DefaultPolicy is the default implementation of the policy interface. 69 | type DefaultPolicy struct { 70 | ID string `json:"id" gorethink:"id"` 71 | Description string `json:"description" gorethink:"description"` 72 | Subjects []string `json:"subjects" gorethink:"subjects"` 73 | Effect string `json:"effect" gorethink:"effect"` 74 | Resources []string `json:"resources" gorethink:"resources"` 75 | Actions []string `json:"actions" gorethink:"actions"` 76 | Conditions Conditions `json:"conditions" gorethink:"conditions"` 77 | Meta []byte `json:"meta" gorethink:"meta"` 78 | } 79 | 80 | // UnmarshalJSON overwrite own policy with values of the given in policy in JSON format 81 | func (p *DefaultPolicy) UnmarshalJSON(data []byte) error { 82 | var pol = struct { 83 | ID string `json:"id" gorethink:"id"` 84 | Description string `json:"description" gorethink:"description"` 85 | Subjects []string `json:"subjects" gorethink:"subjects"` 86 | Effect string `json:"effect" gorethink:"effect"` 87 | Resources []string `json:"resources" gorethink:"resources"` 88 | Actions []string `json:"actions" gorethink:"actions"` 89 | Conditions Conditions `json:"conditions" gorethink:"conditions"` 90 | Meta []byte `json:"meta" gorethink:"meta"` 91 | }{ 92 | Conditions: Conditions{}, 93 | } 94 | 95 | if err := json.Unmarshal(data, &pol); err != nil { 96 | return errors.WithStack(err) 97 | } 98 | 99 | *p = *&DefaultPolicy{ 100 | ID: pol.ID, 101 | Description: pol.Description, 102 | Subjects: pol.Subjects, 103 | Effect: pol.Effect, 104 | Resources: pol.Resources, 105 | Actions: pol.Actions, 106 | Conditions: pol.Conditions, 107 | Meta: pol.Meta, 108 | } 109 | return nil 110 | } 111 | 112 | // UnmarshalMeta parses the policies []byte encoded metadata and stores the result in the value pointed to by v. 113 | func (p *DefaultPolicy) UnmarshalMeta(v interface{}) error { 114 | if err := json.Unmarshal(p.Meta, &v); err != nil { 115 | return errors.WithStack(err) 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // GetID returns the policies id. 122 | func (p *DefaultPolicy) GetID() string { 123 | return p.ID 124 | } 125 | 126 | // GetDescription returns the policies description. 127 | func (p *DefaultPolicy) GetDescription() string { 128 | return p.Description 129 | } 130 | 131 | // GetSubjects returns the policies subjects. 132 | func (p *DefaultPolicy) GetSubjects() []string { 133 | return p.Subjects 134 | } 135 | 136 | // AllowAccess returns true if the policy effect is allow, otherwise false. 137 | func (p *DefaultPolicy) AllowAccess() bool { 138 | return p.Effect == AllowAccess 139 | } 140 | 141 | // GetEffect returns the policies effect which might be 'allow' or 'deny'. 142 | func (p *DefaultPolicy) GetEffect() string { 143 | return p.Effect 144 | } 145 | 146 | // GetResources returns the policies resources. 147 | func (p *DefaultPolicy) GetResources() []string { 148 | return p.Resources 149 | } 150 | 151 | // GetActions returns the policies actions. 152 | func (p *DefaultPolicy) GetActions() []string { 153 | return p.Actions 154 | } 155 | 156 | // GetConditions returns the policies conditions. 157 | func (p *DefaultPolicy) GetConditions() Conditions { 158 | return p.Conditions 159 | } 160 | 161 | // GetMeta returns the policies arbitrary metadata set by the user. 162 | func (p *DefaultPolicy) GetMeta() []byte { 163 | return p.Meta 164 | } 165 | 166 | // GetEndDelimiter returns the delimiter which identifies the end of a regular expression. 167 | func (p *DefaultPolicy) GetEndDelimiter() byte { 168 | return '>' 169 | } 170 | 171 | // GetStartDelimiter returns the delimiter which identifies the beginning of a regular expression. 172 | func (p *DefaultPolicy) GetStartDelimiter() byte { 173 | return '<' 174 | } 175 | -------------------------------------------------------------------------------- /policy_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "encoding/json" 25 | "fmt" 26 | "testing" 27 | 28 | "github.com/stretchr/testify/assert" 29 | "github.com/stretchr/testify/require" 30 | 31 | . "github.com/ory/ladon" 32 | ) 33 | 34 | var policyConditions = Conditions{ 35 | "owner": &EqualsSubjectCondition{}, 36 | } 37 | 38 | var policyCases = []*DefaultPolicy{ 39 | { 40 | ID: "1", 41 | Description: "description", 42 | Subjects: []string{"user"}, 43 | Effect: AllowAccess, 44 | Resources: []string{"articles:<[0-9]+>"}, 45 | Actions: []string{"create", "update"}, 46 | Conditions: policyConditions, 47 | }, 48 | { 49 | Effect: DenyAccess, 50 | Conditions: make(Conditions), 51 | }, 52 | } 53 | 54 | type TestMeta struct { 55 | Key string `json:"key"` 56 | } 57 | 58 | func TestHasAccess(t *testing.T) { 59 | assert.True(t, policyCases[0].AllowAccess()) 60 | assert.False(t, policyCases[1].AllowAccess()) 61 | } 62 | 63 | func TestMarshalling(t *testing.T) { 64 | for k, c := range policyCases { 65 | t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { 66 | var cc = DefaultPolicy{ 67 | Conditions: make(Conditions), 68 | } 69 | data, err := json.Marshal(c) 70 | RequireError(t, false, err) 71 | 72 | json.Unmarshal(data, &cc) 73 | RequireError(t, false, err) 74 | assert.Equal(t, c, &cc) 75 | }) 76 | } 77 | } 78 | 79 | func TestMetaUnmarshalling(t *testing.T) { 80 | var m = TestMeta{ 81 | Key: "test", 82 | } 83 | var mm TestMeta 84 | var p = DefaultPolicy{} 85 | 86 | data, err := json.Marshal(&m) 87 | RequireError(t, false, err) 88 | 89 | p.Meta = data 90 | 91 | err = p.UnmarshalMeta(&mm) 92 | RequireError(t, false, err) 93 | 94 | assert.Equal(t, &m, &mm) 95 | } 96 | 97 | func TestGetters(t *testing.T) { 98 | for _, c := range policyCases { 99 | assert.Equal(t, c.ID, c.GetID()) 100 | assert.Equal(t, c.Description, c.GetDescription()) 101 | assert.Equal(t, c.Resources, c.GetResources()) 102 | assert.Equal(t, c.Subjects, c.GetSubjects()) 103 | assert.Equal(t, len(c.Conditions), len(c.GetConditions())) 104 | assert.Equal(t, c.Effect, c.GetEffect()) 105 | assert.Equal(t, c.Actions, c.GetActions()) 106 | assert.Equal(t, byte('<'), c.GetStartDelimiter()) 107 | assert.Equal(t, byte('>'), c.GetEndDelimiter()) 108 | } 109 | } 110 | 111 | func RequireError(t *testing.T, expectError bool, err error, args ...interface{}) { 112 | if err != nil && !expectError { 113 | t.Logf("Unexpected error: %s\n", err.Error()) 114 | t.Logf("Arguments: %v\n", args) 115 | t.Logf("\n\n") 116 | } 117 | require.Equal(t, expectError, err != nil) 118 | } 119 | -------------------------------------------------------------------------------- /warden.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon 22 | 23 | import "context" 24 | 25 | // Request is the warden's request object. 26 | type Request struct { 27 | // Resource is the resource that access is requested to. 28 | Resource string `json:"resource"` 29 | 30 | // Action is the action that is requested on the resource. 31 | Action string `json:"action"` 32 | 33 | // Subejct is the subject that is requesting access. 34 | Subject string `json:"subject"` 35 | 36 | // Context is the request's environmental context. 37 | Context Context `json:"context"` 38 | } 39 | 40 | // Warden is responsible for deciding if subject s can perform action a on resource r with context c. 41 | type Warden interface { 42 | // IsAllowed returns nil if subject s can perform action a on resource r with context c or an error otherwise. 43 | // if err := guard.IsAllowed(&Request{Resource: "article/1234", Action: "update", Subject: "peter"}); err != nil { 44 | // return errors.New("Not allowed") 45 | // } 46 | IsAllowed(ctx context.Context, r *Request) error 47 | } 48 | -------------------------------------------------------------------------------- /warden_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2016-2018 Aeneas Rekkas 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | * @author Aeneas Rekkas 17 | * @copyright 2015-2018 Aeneas Rekkas 18 | * @license Apache-2.0 19 | */ 20 | 21 | package ladon_test 22 | 23 | import ( 24 | "context" 25 | "fmt" 26 | "testing" 27 | 28 | "github.com/golang/mock/gomock" 29 | "github.com/pkg/errors" 30 | "github.com/stretchr/testify/assert" 31 | 32 | . "github.com/ory/ladon" 33 | ) 34 | 35 | func TestWardenIsGranted(t *testing.T) { 36 | ctrl := gomock.NewController(t) 37 | m := NewMockManager(ctrl) 38 | defer ctrl.Finish() 39 | 40 | w := &Ladon{ 41 | Manager: m, 42 | } 43 | 44 | ctx := context.Background() 45 | 46 | for k, c := range []struct { 47 | r *Request 48 | description string 49 | setup func() 50 | expectErr bool 51 | }{ 52 | { 53 | description: "should fail because no policies are found for peter", 54 | r: &Request{Subject: "peter"}, 55 | setup: func() { 56 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, nil) 57 | }, 58 | expectErr: true, 59 | }, 60 | { 61 | description: "should fail because lookup failure when accessing policies for peter", 62 | r: &Request{Subject: "peter"}, 63 | setup: func() { 64 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, errors.New("asdf")) 65 | }, 66 | expectErr: true, 67 | }, 68 | { 69 | description: "should pass", 70 | r: &Request{ 71 | Subject: "peter", 72 | Resource: "articles:1234", 73 | Action: "view", 74 | }, 75 | setup: func() { 76 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ 77 | Subject: "peter", 78 | Resource: "articles:1234", 79 | Action: "view", 80 | })).Return(Policies{ 81 | &DefaultPolicy{ 82 | Subjects: []string{""}, 83 | Effect: AllowAccess, 84 | Resources: []string{"articles:<[0-9]+>"}, 85 | Actions: []string{"view"}, 86 | }, 87 | }, nil) 88 | }, 89 | expectErr: false, 90 | }, 91 | { 92 | description: "should fail because subjects don't match (unlikely event)", 93 | r: &Request{ 94 | Subject: "ken", 95 | Resource: "articles:1234", 96 | Action: "view", 97 | }, 98 | setup: func() { 99 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ 100 | Subject: "ken", 101 | Resource: "articles:1234", 102 | Action: "view", 103 | })).Return(Policies{ 104 | &DefaultPolicy{ 105 | Subjects: []string{""}, 106 | Effect: AllowAccess, 107 | Resources: []string{"articles:<[0-9]+>"}, 108 | Actions: []string{"view"}, 109 | }, 110 | }, nil) 111 | }, 112 | expectErr: true, 113 | }, 114 | { 115 | description: "should fail because resources mismatch", 116 | r: &Request{ 117 | Subject: "ken", 118 | Resource: "printers:321", 119 | Action: "view", 120 | }, 121 | setup: func() { 122 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ 123 | Subject: "ken", 124 | Resource: "printers:321", 125 | Action: "view", 126 | })).Return(Policies{ 127 | &DefaultPolicy{ 128 | Subjects: []string{"ken", "peter"}, 129 | Effect: AllowAccess, 130 | Resources: []string{"articles:<[0-9]+>"}, 131 | Actions: []string{"view"}, 132 | }, 133 | }, nil) 134 | }, 135 | expectErr: true, 136 | }, 137 | { 138 | description: "should fail because action mismatch", 139 | r: &Request{ 140 | Subject: "ken", 141 | Resource: "articles:321", 142 | Action: "view", 143 | }, 144 | setup: func() { 145 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ 146 | Subject: "ken", 147 | Resource: "articles:321", 148 | Action: "view", 149 | })).Return(Policies{ 150 | &DefaultPolicy{ 151 | Subjects: []string{"ken", "peter"}, 152 | Effect: AllowAccess, 153 | Resources: []string{"articles:<[0-9]+>"}, 154 | Actions: []string{""}, 155 | }, 156 | }, nil) 157 | }, 158 | expectErr: true, 159 | }, 160 | { 161 | description: "should pass", 162 | r: &Request{ 163 | Subject: "ken", 164 | Resource: "articles:321", 165 | Action: "foo", 166 | }, 167 | setup: func() { 168 | m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ 169 | Subject: "ken", 170 | Resource: "articles:321", 171 | Action: "foo", 172 | })).Return(Policies{ 173 | &DefaultPolicy{ 174 | Subjects: []string{"ken", "peter"}, 175 | Effect: AllowAccess, 176 | Resources: []string{"articles:<[0-9]+>"}, 177 | Actions: []string{""}, 178 | }, 179 | }, nil) 180 | }, 181 | expectErr: false, 182 | }, 183 | } { 184 | t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { 185 | c.setup() 186 | err := w.IsAllowed(ctx, c.r) 187 | if c.expectErr { 188 | assert.NotNil(t, err) 189 | } else { 190 | assert.Nil(t, err) 191 | } 192 | }) 193 | } 194 | } 195 | --------------------------------------------------------------------------------