diff --git a/.bin/go-licenses b/.bin/go-licenses new file mode 100755 index 0000000..279450e Binary files /dev/null and b/.bin/go-licenses differ diff --git a/.bin/license-engine.sh b/.bin/license-engine.sh new file mode 100755 index 0000000..7d6ec1c --- /dev/null +++ b/.bin/license-engine.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# This script detects non-compliant licenses in the output of language-specific license checkers. + +# These licenses are allowed. +# These are the exact and complete license strings for 100% legal certainty, no regexes. +ALLOWED_LICENSES=( + '0BSD' + 'AFLv2.1' + 'AFLv2.1,BSD' + '(AFL-2.1 OR BSD-3-Clause)' + 'Apache 2.0' + 'Apache-2.0' + '(Apache-2.0 OR MPL-1.1)' + 'Apache-2.0 AND MIT' + 'Apache License, Version 2.0' + 'Apache*' + 'Artistic-2.0' + 'BlueOak-1.0.0' + 'BSD' + 'BSD*' + 'BSD-2-Clause' + '(BSD-2-Clause OR MIT OR Apache-2.0)' + 'BSD-3-Clause' + '(BSD-3-Clause OR GPL-2.0)' + 'BSD-3-Clause OR MIT' + '(BSD-3-Clause AND Apache-2.0)' + 'CC0-1.0' + 'CC-BY-3.0' + 'CC-BY-4.0' + '(CC-BY-4.0 AND MIT)' + 'ISC' + 'ISC*' + 'LGPL-2.1' # LGPL allows commercial use, requires only that modifications to LGPL-protected libraries are published under a GPL-compatible license + 'MIT' + 'MIT*' + 'MIT-0' + 'MIT AND ISC' + '(MIT AND BSD-3-Clause)' + '(MIT AND Zlib)' + '(MIT OR Apache-2.0)' + '(MIT OR CC0-1.0)' + '(MIT OR GPL-2.0)' + 'MPL-2.0' + '(MPL-2.0 OR Apache-2.0)' + 'Public Domain' + 'Python-2.0' # the Python-2.0 is a permissive license, see https://en.wikipedia.org/wiki/Python_License + 'Unlicense' + 'WTFPL' + 'WTFPL OR ISC' + '(WTFPL OR MIT)' + '(MIT OR WTFPL)' + '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 +) + +# These modules don't work with the current license checkers +# and have been manually verified to have a compatible license (regex format). +APPROVED_MODULES=( + 'https://github.com/ory-corp/cloud/' # Ory IP + 'github.com/ory/hydra-client-go' # Apache-2.0 + 'github.com/ory/hydra-client-go/v2' # Apache-2.0 + 'github.com/ory/kratos-client-go' # Apache-2.0 + 'github.com/gobuffalo/github_flavored_markdown' # MIT + '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) + 'https://github.com/iconify/iconify/packages/react' # MIT: license is in root of monorepo at https://github.com/iconify/iconify/blob/main/license.txt + 'github.com/gobuffalo/.*' # MIT: license is in root of monorepo at https://github.com/gobuffalo/github_flavored_markdown/blob/main/LICENSE + 'github.com/ory-corp/cloud/.*' # Ory IP + 'github.com/golang/freetype/.*' # FreeType license: https://freetype.sourceforge.net/FTL.TXT + '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 + '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 + 'github.com/ory/client-go' # Apache-2.0 + 'github.com/ian-kent/linkio' # BSD - https://github.com/ian-kent/linkio/blob/97566b8728870dac1c9863ba5b0f237c39166879/linkio.go#L1-L3 + 'github.com/t-k/fluent-logger-golang/fluent' # Apache-2.0 https://github.com/t-k/fluent-logger-golang/blob/master/LICENSE + 'github.com/jmespath/go-jmespath' # Apache-2.0 https://github.com/jmespath/go-jmespath/blob/master/LICENSE + 'github.com/ory/keto/proto/ory/keto/opl/v1alpha1' # Apache-2.0 - submodule of keto + 'github.com/ory/keto/proto/ory/keto/relation_tuples/v1alpha2' # Apache-2.0 - submodule of keto + '@ory-corp/.*' # Ory IP + 'github.com/apache/arrow/.*' # Apache-2.0 https://github.com/apache/arrow/blob/main/LICENSE.txt + 'github.com/ory-corp/webhook-target' # Ory IP + '@ory/keto-grpc-client.*' # Apache-2.0 - submodule of keto + 'golden-fleece@1.0.9' # MIT: https://github.com/Rich-Harris/golden-fleece/blob/master/LICENSE + 'github.com/gogo/googleapis/.*' # Apache-2.0 https://github.com/gogo/googleapis/blob/master/LICENSE +) + +# These lines in the output should be ignored (plain text, no regex). +IGNORE_LINES=( + '"module name","licenses"' # header of license output for Node.js +) + +echo_green() { + printf "\e[1;92m%s\e[0m\n" "$@" +} + +echo_red() { + printf "\e[0;91m%s\e[0m\n" "$@" +} + +# capture STDIN +input=$(cat -) + +# remove ignored lines +for ignored in "${IGNORE_LINES[@]}"; do + input=$(echo "$input" | grep -vF "$ignored") +done + +# remove pre-approved modules +for approved in "${APPROVED_MODULES[@]}"; do + input=$(echo "$input" | grep -vE "\"${approved}\"") + input=$(echo "$input" | grep -vE "\"Custom: ${approved}\"") +done + +# remove allowed licenses +for allowed in "${ALLOWED_LICENSES[@]}"; do + input=$(echo "$input" | grep -vF "\"${allowed}\"") +done + +# anything left in the input at this point is a module with an invalid license + +# print outcome +if [ -z "$input" ]; then + echo_green "Licenses are okay." +else + echo_red "Unknown licenses found!" + echo "$input" + exit 1 +fi diff --git a/.bin/license-template-go.tpl b/.bin/license-template-go.tpl new file mode 100644 index 0000000..f5ae6f7 --- /dev/null +++ b/.bin/license-template-go.tpl @@ -0,0 +1,3 @@ +{{ range . }} +"{{.Name}}","{{.LicenseName}}" +{{- end }} diff --git a/.bin/licenses b/.bin/licenses new file mode 100755 index 0000000..25d3c9d --- /dev/null +++ b/.bin/licenses @@ -0,0 +1,8 @@ +#!/bin/sh +set -e + +# Get the directory where this script is located +bin_dir="$(cd "$(dirname "$0")" && pwd)" + +{ echo "Checking licenses ..."; } 2>/dev/null +"${bin_dir}/list-licenses" | "${bin_dir}/license-engine.sh" diff --git a/.bin/list-licenses b/.bin/list-licenses new file mode 100755 index 0000000..8a238d9 --- /dev/null +++ b/.bin/list-licenses @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +bin_dir="$(cd "$(dirname "$0")" && pwd)" + +# list Node licenses +if [ -f package.json ]; then + if jq -e '.dependencies and (.dependencies | keys | length > 0)' package.json >/dev/null; then + npx --yes license-checker --production --csv --excludePrivatePackages --customPath "${bin_dir}"/license-template-node.json | grep -v '^$' + echo + else + echo "No dependencies found in package.json" >&2 + echo + fi +fi + +# list Go licenses +if [ -f go.mod ]; then + # List all direct Go module dependencies, transform their paths to root module paths + # (e.g., github.com/ory/x instead of github.com/ory/x/foo/bar), and generate a license report + # for each unique root module. This ensures that the license report is generated for the root + # module of a repository, where licenses are typically defined. + go_modules=$( + go list -f "{{if not .Indirect}}{{.Path}}{{end}}" -m ... | + sort -u | + awk -F/ '{ if ($1 == "github.com" && NF >= 3) { print $1"/"$2"/"$3 } else { print } }' | + sort -u + ) + if [ -z "$go_modules" ]; then + echo "No Go modules found" >&2 + else + # Workaround until https://github.com/google/go-licenses/issues/307 is fixed + # .bin/go-licenses report "$module_name" --template .bin/license-template-go.tpl 2>/dev/null + # + echo "$go_modules" | xargs -I {} sh -c '.bin/go-licenses report --template .bin/license-template-go.tpl {}' | grep -v '^$' + echo + fi +fi diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..c440360 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/FUNDING.yml + +# These are supported funding model platforms + +# github: +patreon: _ory +open_collective: ory diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 0000000..b215d76 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,122 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/BUG-REPORT.yml + +description: "Create a bug report" +labels: + - bug +name: "Bug Report" +body: + - attributes: + value: "Thank you for taking the time to fill out this bug report!\n" + type: markdown + - attributes: + label: "Preflight checklist" + options: + - label: + "I could not find a solution in the existing issues, docs, nor + discussions." + required: true + - label: + "I agree to follow this project's [Code of + Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/l/sign-up-newsletter)." + id: checklist + type: checkboxes + - attributes: + description: + "Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting." + label: "Ory Network Project" + placeholder: "https://.projects.oryapis.com" + id: ory-network-project + type: input + - attributes: + description: "A clear and concise description of what the bug is." + label: "Describe the bug" + placeholder: "Tell us what you see!" + id: describe-bug + type: textarea + validations: + required: true + - attributes: + description: | + Clear, formatted, and easy to follow steps to reproduce the behavior: + placeholder: | + Steps to reproduce the behavior: + + 1. Run `docker run ....` + 2. Make API Request to with `curl ...` + 3. Request fails with response: `{"some": "error"}` + label: "Reproducing the bug" + id: reproduce-bug + type: textarea + validations: + required: true + - attributes: + description: + "Please copy and paste any relevant log output. This will be + automatically formatted into code, so no need for backticks. Please + redact any sensitive information" + label: "Relevant log output" + render: shell + placeholder: | + log=error .... + id: logs + type: textarea + - attributes: + description: + "Please copy and paste any relevant configuration. This will be + automatically formatted into code, so no need for backticks. Please + redact any sensitive information!" + label: "Relevant configuration" + render: yml + placeholder: | + server: + admin: + port: 1234 + id: config + type: textarea + - attributes: + description: "What version of our software are you running?" + label: Version + id: version + type: input + validations: + required: true + - attributes: + label: "On which operating system are you observing this issue?" + options: + - Ory Network + - macOS + - Linux + - Windows + - FreeBSD + - Other + id: operating-system + type: dropdown + - attributes: + label: "In which environment are you deploying?" + options: + - Ory Network + - Docker + - "Docker Compose" + - "Kubernetes with Helm" + - Kubernetes + - Binary + - Other + id: deployment + type: dropdown + - attributes: + description: "Add any other context about the problem here." + label: Additional Context + id: additional + type: textarea diff --git a/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml new file mode 100644 index 0000000..4881dd5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml @@ -0,0 +1,125 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/DESIGN-DOC.yml + +description: + "A design document is needed for non-trivial changes to the code base." +labels: + - rfc +name: "Design Document" +body: + - attributes: + value: | + Thank you for writing this design document. + + 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. + + Ory is leaning heavily on [Google's design docs process](https://www.industrialempathy.com/posts/design-docs-at-google/) + and [Golang Proposals](https://github.com/golang/proposal). + + Writing a design doc before contributing your change ensures that your ideas are checked with + the community and maintainers. It will save you a lot of time developing things that might need to be changed + after code reviews, and your pull requests will be merged faster. + type: markdown + - attributes: + label: "Preflight checklist" + options: + - label: + "I could not find a solution in the existing issues, docs, nor + discussions." + required: true + - label: + "I agree to follow this project's [Code of + Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/l/sign-up-newsletter)." + id: checklist + type: checkboxes + - attributes: + description: + "Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting." + label: "Ory Network Project" + placeholder: "https://.projects.oryapis.com" + id: ory-network-project + type: input + - attributes: + description: | + 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. + label: "Context and scope" + id: scope + type: textarea + validations: + required: true + + - attributes: + description: | + 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. + label: "Goals and non-goals" + id: goals + type: textarea + validations: + required: true + + - attributes: + description: | + This section should start with an overview and then go into details. + 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. + + 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. + label: "The design" + id: design + type: textarea + validations: + required: true + + - attributes: + description: | + 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. + label: "APIs" + id: apis + type: textarea + + - attributes: + description: | + 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. + label: "Data storage" + id: persistence + type: textarea + + - attributes: + description: | + 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. + label: "Code and pseudo-code" + id: pseudocode + type: textarea + + - attributes: + description: | + 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. + + 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. + + 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. + + 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. + label: "Degree of constraint" + id: constrait + type: textarea + + - attributes: + description: | + 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. + + 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. + + label: Alternatives considered + id: alternatives + type: textarea diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml new file mode 100644 index 0000000..d1bcd37 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml @@ -0,0 +1,86 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml + +description: + "Suggest an idea for this project without a plan for implementation" +labels: + - feat +name: "Feature Request" +body: + - attributes: + value: | + Thank you for suggesting an idea for this project! + + 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! + type: markdown + - attributes: + label: "Preflight checklist" + options: + - label: + "I could not find a solution in the existing issues, docs, nor + discussions." + required: true + - label: + "I agree to follow this project's [Code of + Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/ladon/blob/master/CONTRIBUTING.md)." + required: true + - label: + "I have joined the [Ory Community Slack](https://slack.ory.com)." + - label: + "I am signed up to the [Ory Security Patch + Newsletter](https://www.ory.com/l/sign-up-newsletter)." + id: checklist + type: checkboxes + - attributes: + description: + "Enter the slug or API URL of the affected Ory Network project. Leave + empty when you are self-hosting." + label: "Ory Network Project" + placeholder: "https://.projects.oryapis.com" + id: ory-network-project + type: input + - attributes: + description: + "Is your feature request related to a problem? Please describe." + label: "Describe your problem" + placeholder: + "A clear and concise description of what the problem is. Ex. I'm always + frustrated when [...]" + id: problem + type: textarea + validations: + required: true + - attributes: + description: | + Describe the solution you'd like + placeholder: | + A clear and concise description of what you want to happen. + label: "Describe your ideal solution" + id: solution + type: textarea + validations: + required: true + - attributes: + description: "Describe alternatives you've considered" + label: "Workarounds or alternatives" + id: alternatives + type: textarea + validations: + required: true + - attributes: + description: "What version of our software are you running?" + label: Version + id: version + type: input + validations: + required: true + - attributes: + description: + "Add any other context or screenshots about the feature request here." + label: Additional Context + id: additional + type: textarea diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4d0fda8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/ISSUE_TEMPLATE/config.yml + +blank_issues_enabled: false +contact_links: + - name: Ory ladon Forum + url: https://github.com/orgs/ory/discussions + about: + Please ask and answer questions here, show your implementations and + discuss ideas. + - name: Ory Chat + url: https://www.ory.com/chat + about: + Hang out with other Ory community members to ask and answer questions. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7a1e35e..57e3e47 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,7 +1,7 @@ + +## Related Issue or Design Document + + + +## Checklist + + + +- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md) and signed the CLA. +- [ ] I have referenced an issue containing the design document if my change introduces a new feature. +- [ ] I have read the [security policy](../security/policy). +- [ ] I confirm that this pull request does not address a security vulnerability. + If this pull request addresses a security vulnerability, + I confirm that I got approval (please contact [security@ory.com](mailto:security@ory.com)) from the maintainers to push the changes. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] I have added the necessary documentation within the code base (if appropriate). + +## Further comments + + diff --git a/.github/workflows/closed_references.yml b/.github/workflows/closed_references.yml new file mode 100644 index 0000000..9a1b483 --- /dev/null +++ b/.github/workflows/closed_references.yml @@ -0,0 +1,30 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/closed_references.yml + +name: Closed Reference Notifier + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + issueLimit: + description: Max. number of issues to create + required: true + default: "5" + +jobs: + find_closed_references: + if: github.repository_owner == 'ory' + runs-on: ubuntu-latest + name: Find closed references + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2-beta + with: + node-version: "14" + - uses: ory/closed-reference-notifier@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issueLabels: upstream,good first issue,help wanted + issueLimit: ${{ github.event.inputs.issueLimit || '5' }} diff --git a/.github/workflows/conventional_commits.yml b/.github/workflows/conventional_commits.yml new file mode 100644 index 0000000..c4d3905 --- /dev/null +++ b/.github/workflows/conventional_commits.yml @@ -0,0 +1,59 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/conventional_commits.yml + +name: Conventional commits + +# This GitHub CI Action enforces that pull request titles follow conventional commits. +# More info at https://www.conventionalcommits.org. +# +# The Ory-wide defaults for commit titles and scopes are below. +# Your repository can add/replace elements via a configuration file at the path below. +# More info at https://github.com/ory/ci/blob/master/conventional_commit_config/README.md + +on: + pull_request_target: + types: + - edited + - opened + - ready_for_review + - reopened + # pull_request: # for debugging, uses config in local branch but supports only Pull Requests from this repo + +jobs: + main: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - id: config + uses: ory/ci/conventional_commit_config@master + with: + config_path: .github/conventional_commits.json + default_types: | + feat + fix + revert + docs + style + refactor + test + build + autogen + security + ci + chore + default_scopes: | + deps + docs + default_require_scope: false + - uses: amannn/action-semantic-pull-request@v4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: ${{ steps.config.outputs.types }} + scopes: ${{ steps.config.outputs.scopes }} + requireScope: ${{ steps.config.outputs.requireScope }} + subjectPattern: ^(?![A-Z]).+$ + subjectPatternError: | + The subject should start with a lowercase letter, yours is uppercase: + "{subject}" diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..ca84889 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,25 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go + +name: Go + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.19 + + - name: Test + run: go test -v ./... diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 0000000..e903667 --- /dev/null +++ b/.github/workflows/labels.yml @@ -0,0 +1,25 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/labels.yml + +name: Synchronize Issue Labels + +on: + workflow_dispatch: + push: + branches: + - master + +jobs: + milestone: + if: github.repository_owner == 'ory' + name: Synchronize Issue Labels + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Synchronize Issue Labels + uses: ory/label-sync-action@v0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + dry: false + forced: true diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml new file mode 100644 index 0000000..4d99650 --- /dev/null +++ b/.github/workflows/licenses.yml @@ -0,0 +1,35 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/licenses.yml + +name: Licenses + +on: + pull_request: + push: + branches: + - main + - v3 + - master + +jobs: + licenses: + name: License compliance + runs-on: ubuntu-latest + steps: + - name: Install script + uses: ory/ci/licenses/setup@master + with: + token: ${{ secrets.ORY_BOT_PAT || secrets.GITHUB_TOKEN }} + - name: Check licenses + uses: ory/ci/licenses/check@master + - name: Write, commit, push licenses + uses: ory/ci/licenses/write@master + if: + ${{ github.ref == 'refs/heads/main' || github.ref == + 'refs/heads/master' || github.ref == 'refs/heads/v3' }} + with: + author-email: + ${{ secrets.ORY_BOT_PAT && + '60093411+ory-bot@users.noreply.github.com' || + format('{0}@users.noreply.github.com', github.actor) }} + author-name: ${{ secrets.ORY_BOT_PAT && 'ory-bot' || github.actor }} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..ac48a5e --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,47 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/workflows/stale.yml + +name: "Close Stale Issues" +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + stale: + if: github.repository_owner == 'ory' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: | + Hello contributors! + + 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 + + - open a PR referencing and resolving the issue; + - leave a comment on it and discuss ideas on how you could contribute towards resolving it; + - leave a comment and describe in detail why this issue is critical for your use case; + - open a new issue with updated details and a plan for resolving the issue. + + 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. + + 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. + + 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/). + + The motivation for this automation is to help prioritize issues in the backlog and not ignore, reject, or belittle anyone. + + 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. + + 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! + + Thank you 🙏✌️ + stale-issue-label: "stale" + exempt-issue-labels: "bug,blocking,docs,backlog" + days-before-stale: 365 + days-before-close: 30 + exempt-milestones: true + exempt-assignees: true + only-pr-labels: "stale" diff --git a/.reference-ignore b/.reference-ignore new file mode 100644 index 0000000..eee2a89 --- /dev/null +++ b/.reference-ignore @@ -0,0 +1,3 @@ +**/node_modules +docs +CHANGELOG.md diff --git a/.reports/dep-licenses.csv b/.reports/dep-licenses.csv new file mode 100644 index 0000000..af265c5 --- /dev/null +++ b/.reports/dep-licenses.csv @@ -0,0 +1,11 @@ +"github.com/dlclark/regexp2","MIT" +"github.com/hashicorp/golang-lru","MPL-2.0" +"github.com/dlclark/regexp2","MIT" +"github.com/hashicorp/golang-lru","MPL-2.0" +"github.com/ory/ladon","Apache-2.0" +"github.com/pkg/errors","BSD-2-Clause" +"github.com/ory/pagination","Apache-2.0" +"github.com/google/uuid","BSD-3-Clause" +"github.com/pborman/uuid","BSD-3-Clause" +"github.com/pkg/errors","BSD-2-Clause" + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fc7d9bc --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,145 @@ + + + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Open Source Community Support + +Ory Open source software is collaborative and based on contributions by +developers in the Ory community. There is no obligation from Ory to help with +individual problems. If Ory open source software is used in production in a +for-profit company or enterprise environment, we mandate a paid support contract +where Ory is obligated under their service level agreements (SLAs) to offer a +defined level of availability and responsibility. For more information about +paid support please contact us at sales@ory.com. + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[office@ory.com](mailto:office@ory.com). All complaints will be reviewed and +investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4c95fd..cd978e5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,119 +1,253 @@ -# Contribution Guide + + -We welcome and encourage community contributions to Ladon. - -Since the project is still unstable, there are specific priorities for development. Pull requests that do not address these priorities will not be accepted until Ladon is production ready. - -Please familiarize yourself with the Contribution Guidelines and Project Roadmap before contributing. - -There are many ways to help Ladon besides contributing code: - - - Fix bugs or file issues - - Improve the documentation +# Contribute to Ory ladon -**Table of Contents** -- [Contributing Code](#contributing-code) -- [Code Style](#code-style) -- [Developer’s Certificate of Origin](#developer%E2%80%99s-certificate-of-origin) -- [Pull request procedure](#pull-request-procedure) +- [Introduction](#introduction) +- [FAQ](#faq) +- [How can I contribute?](#how-can-i-contribute) +- [Communication](#communication) +- [Contribute examples or community projects](#contribute-examples-or-community-projects) +- [Contribute code](#contribute-code) +- [Contribute documentation](#contribute-documentation) +- [Disclosing vulnerabilities](#disclosing-vulnerabilities) +- [Code style](#code-style) + - [Working with forks](#working-with-forks) - [Conduct](#conduct) -## Contributing Code +## Introduction -Unless you are fixing a known bug, we **strongly** recommend discussing it with the core team via a GitHub issue before getting started to ensure your work is consistent with Ladon's roadmap and architecture. +_Please note_: We take Ory ladon's security and our users' trust very +seriously. If you believe you have found a security issue in Ory ladon, +please disclose it by contacting us at security@ory.com. -All contributions are made via pull request. Note that **all patches from all contributors get reviewed**. After a pull request is made other contributors will offer feedback, and if the patch passes review a maintainer will accept it with a comment. When pull requests fail testing, authors are expected to update their pull requests to address the failures until the tests pass and the pull request merges successfully. +There are many ways in which you can contribute. The goal of this document is to +provide a high-level overview of how you can get involved in Ory. -At least one review from a maintainer is required for all patches (even patches from maintainers). +As a potential contributor, your changes and ideas are welcome at any hour of +the day or night, on weekdays, weekends, and holidays. Please do not ever +hesitate to ask a question or send a pull request. -Reviewers should leave a "LGTM" comment once they are satisfied with the patch. If the patch was submitted by a maintainer with write access, the pull request should be merged by the submitter after review. +If you are unsure, just ask or submit the issue or pull request anyways. You +won't be yelled at for giving it your best effort. The worst that can happen is +that you'll be politely asked to change something. We appreciate any sort of +contributions and don't want a wall of rules to get in the way of that. -## Code Style +That said, if you want to ensure that a pull request is likely to be merged, +talk to us! You can find out our thoughts and ensure that your contribution +won't clash with Ory +ladon's direction. A great way to +do this is via +[Ory ladon Discussions](https://github.com/orgs/ory/discussions) +or the [Ory Chat](https://www.ory.com/chat). -Please follow these guidelines when formatting source code: +## FAQ -* Go code should match the output of `gofmt -s` +- I am new to the community. Where can I find the + [Ory Community Code of Conduct?](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md) -## Developer’s Certificate of Origin +- I have a question. Where can I get + [answers to questions regarding Ory ladon?](#communication) -All contributions must include acceptance of the DCO: +- I would like to contribute but I am not sure how. Are there + [easy ways to contribute?](#how-can-i-contribute) + [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) -```text -Developer Certificate of Origin -Version 1.1 +- I want to talk to other Ory ladon users. + [How can I become a part of the community?](#communication) -Copyright (C) 2004, 2006 The Linux Foundation and its contributors. -660 York Street, Suite 102, -San Francisco, CA 94110 USA +- I would like to know what I am agreeing to when I contribute to Ory + ladon. + Does Ory have + [a Contributors License Agreement?](https://cla-assistant.io/ory/ladon) -Everyone is permitted to copy and distribute verbatim copies of this -license document, but changing it is not allowed. +- I would like updates about new versions of Ory ladon. + [How are new releases announced?](https://www.ory.com/l/sign-up-newsletter) +## How can I contribute? -Developer's Certificate of Origin 1.1 +If you want to start to contribute code right away, take a look at the +[list of good first issues](https://github.com/ory/ladon/labels/good%20first%20issue). -By making a contribution to this project, I certify that: +There are many other ways you can contribute. Here are a few things you can do +to help out: -(a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or +- **Give us a star.** It may not seem like much, but it really makes a + difference. This is something that everyone can do to help out Ory ladon. + Github stars help the project gain visibility and stand out. -(b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or +- **Join the community.** Sometimes helping people can be as easy as listening + to their problems and offering a different perspective. Join our Slack, have a + look at discussions in the forum and take part in community events. More info + on this in [Communication](#communication). -(c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. +- **Answer discussions.** At all times, there are several unanswered discussions + on GitHub. You can see an + [overview here](https://github.com/discussions?discussions_q=is%3Aunanswered+org%3Aory+sort%3Aupdated-desc). + If you think you know an answer or can provide some information that might + help, please share it! Bonus: You get GitHub achievements for answered + discussions. -(d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. -``` +- **Help with open issues.** We have a lot of open issues for Ory ladon and + some of them may lack necessary information, some are duplicates of older + issues. You can help out by guiding people through the process of filling out + the issue template, asking for clarifying information or pointing them to + existing issues that match their description of the problem. -To accept the DCO, simply add this line to each commit message with your name and email address (`git commit -s` will do this for you): +- **Review documentation changes.** Most documentation just needs a review for + proper spelling and grammar. If you think a document can be improved in any + way, feel free to hit the `edit` button at the top of the page. More info on + contributing to the documentation [here](#contribute-documentation). -```text -Signed-off-by: Jane Example -``` +- **Help with tests.** Pull requests may lack proper tests or test plans. These + are needed for the change to be implemented safely. + +## Communication + +We use [Slack](https://www.ory.com/chat). You are welcome to drop in and ask +questions, discuss bugs and feature requests, talk to other users of Ory, etc. + +Check out [Ory ladon Discussions](https://github.com/orgs/ory/discussions). This is a great place for +in-depth discussions and lots of code examples, logs and similar data. + +You can also join our community calls if you want to speak to the Ory team +directly or ask some questions. You can find more info and participate in +[Slack](https://www.ory.com/chat) in the #community-call channel. -For legal reasons, no anonymous or pseudonymous contributions are accepted ([contact us](mailto:aeneas@ory.am) if this is an issue). +If you want to receive regular notifications about updates to Ory ladon, +consider joining the mailing list. We will _only_ send you vital information on +the projects that you are interested in. -## Pull request procedure +Also, [follow us on Twitter](https://twitter.com/orycorp). -To make a pull request, you will need a GitHub account; if you are unclear on this process, see GitHub's documentation on [forking](https://help.github.com/articles/fork-a-repo) and [pull requests](https://help.github.com/articles/using-pull-requests). Pull requests should be targeted at the `master` branch. Before creating a pull request, go through this checklist: +## Contribute examples or community projects + +One of the most impactful ways to contribute is by adding code examples or other +Ory-related code. You can find an overview of community code in the +[awesome-ory](https://github.com/ory/awesome-ory) repository. + +_If you would like to contribute a new example, we would love to hear from you!_ + +Please [open a pull request at awesome-ory](https://github.com/ory/awesome-ory/) +to add your example or Ory-related project to the awesome-ory README. + +## Contribute code + +Unless you are fixing a known bug, we **strongly** recommend discussing it with +the core team via a GitHub issue or [in our chat](https://www.ory.com/chat) +before getting started to ensure your work is consistent with Ory ladon's +roadmap and architecture. + +All contributions are made via pull requests. To make a pull request, you will +need a GitHub account; if you are unclear on this process, see GitHub's +documentation on [forking](https://help.github.com/articles/fork-a-repo) and +[pull requests](https://help.github.com/articles/using-pull-requests). Pull +requests should be targeted at the `master` branch. Before creating a pull +request, go through this checklist: 1. Create a feature branch off of `master` so that changes do not get mixed up. -1. [Rebase](https://git-scm.com/book/en/Git-Branching-Rebasing) your local changes against the `master` branch. -1. Run the full project test suite with the `go test ./...` (or equivalent) command and confirm that it passes. -1. Run `gofmt -s` (if the project is written in Go). -1. Accept the Developer's Certificate of Origin on all commits (see above). -1. Ensure that each commit has a subsystem prefix (ex: `controller: `). +1. [Rebase](http://git-scm.com/book/en/Git-Branching-Rebasing) your local + changes against the `master` branch. +1. Run the full project test suite with the `go test -tags sqlite ./...` (or + equivalent) command and confirm that it passes. +1. Run `make format` +1. Add a descriptive prefix to commits. This ensures a uniform commit history + and helps structure the changelog. Please refer to this + [Convential Commits configuration](https://github.com/ory/ladon/blob/master/.github/workflows/conventional_commits.yml) + for the list of accepted prefixes. You can read more about the Conventional + Commit specification + [at their site](https://www.conventionalcommits.org/en/v1.0.0/). + +If a pull request is not ready to be reviewed yet +[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). + +Before your contributions can be reviewed you need to sign our +[Contributor License Agreement](https://cla-assistant.io/ory/ladon). + +This agreement defines the terms under which your code is contributed to Ory. +More specifically it declares that you have the right to, and actually do, grant +us the rights to use your contribution. You can see the Apache 2.0 license under +which our projects are published +[here](https://github.com/ory/meta/blob/master/LICENSE). -Pull requests will be treated as "review requests," and maintainers will give feedback on the style and substance of the patch. +When pull requests fail the automated testing stages (for example unit or E2E +tests), authors are expected to update their pull requests to address the +failures until the tests pass. + +Pull requests eligible for review + +1. follow the repository's code formatting conventions; +2. include tests that prove that the change works as intended and does not add + regressions; +3. document the changes in the code and/or the project's documentation; +4. pass the CI pipeline; +5. have signed our + [Contributor License Agreement](https://cla-assistant.io/ory/ladon); +6. include a proper git commit message following the + [Conventional Commit Specification](https://www.conventionalcommits.org/en/v1.0.0/). + +If all of these items are checked, the pull request is ready to be reviewed and +you should change the status to "Ready for review" and +[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). + +Reviewers will approve the pull request once they are satisfied with the patch. + +## Contribute documentation + +Please provide documentation when changing, removing, or adding features. All +Ory Documentation resides in the +[Ory documentation repository](https://github.com/ory/docs/). For further +instructions please head over to the Ory Documentation +[README.md](https://github.com/ory/docs/blob/master/README.md). + +## Disclosing vulnerabilities + +Please disclose vulnerabilities exclusively to +[security@ory.com](mailto:security@ory.com). Do not use GitHub issues. + +## Code style + +Please run `make format` to format all source code following the Ory standard. + +### Working with forks + +```bash +# First you clone the original repository +git clone git@github.com:ory/ory/ladon.git + +# Next you add a git remote that is your fork: +git remote add fork git@github.com:/ory/ladon.git + +# Next you fetch the latest changes from origin for master: +git fetch origin +git checkout master +git pull --rebase + +# Next you create a new feature branch off of master: +git checkout my-feature-branch + +# Now you do your work and commit your changes: +git add -A +git commit -a -m "fix: this is the subject line" -m "This is the body line. Closes #123" + +# And the last step is pushing this to your fork +git push -u fork my-feature-branch +``` -Normally, all pull requests must include tests that test your change. Occasionally, a change will be very difficult to test for. In those cases, please include a note in your commit message explaining why. +Now go to the project's GitHub Pull Request page and click "New pull request" ## Conduct -Whether you are a regular contributor or a newcomer, we care about making this community a safe place for you and we've got your back. +Whether you are a regular contributor or a newcomer, we care about making this +community a safe place for you and we've got your back. -* We are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, disability, ethnicity, religion, or similar personal characteristic. -* Please avoid using nicknames that might detract from a friendly, safe and welcoming environment for all. -* Be kind and courteous. There is no need to be mean or rude. -* We will exclude you from interaction if you insult, demean or harass anyone. In particular, we do not tolerate behavior that excludes people in socially marginalized groups. -* Private harassment is also unacceptable. No matter who you are, if you feel you have been or are being harassed or made uncomfortable by a community member, please contact one of the channel ops or a member of the Ladon core team immediately. -* Likewise any spamming, trolling, flaming, baiting or other attention-stealing behaviour is not welcome. +[Ory Community Code of Conduct](https://github.com/ory/ladon/blob/master/CODE_OF_CONDUCT.md) -We welcome discussion about creating a welcoming, safe, and productive environment for the community. If you have any questions, feedback, or concerns please let us know with a GitHub issue. +We welcome discussion about creating a welcoming, safe, and productive +environment for the community. If you have any questions, feedback, or concerns +[please let us know](https://www.ory.com/chat). diff --git a/LICENSE b/LICENSE index f6872a4..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Apache License + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +178,7 @@ Apache License APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,17 +186,16 @@ Apache License same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015-2018 Aeneas Rekkas + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - https://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/README.md b/README.md index 1fd87bd..c841845 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@

ORY Ladon - Policy-based Access Control

-[![Join the chat at https://discord.gg/PAMQWkr](https://img.shields.io/badge/join-chat-00cc99.svg)](https://discord.gg/PAMQWkr) -[![Join newsletter](https://img.shields.io/badge/join-newsletter-00cc99.svg)](http://eepurl.com/bKT3N9) +[![Join the chat at https://www.ory.sh/chat](https://img.shields.io/badge/join-chat-00cc99.svg)](https://www.ory.sh/chat) +[![Join newsletter](https://img.shields.io/badge/join-newsletter-00cc99.svg)](https://www.ory.sh/l/sign-up-newsletter) [![Build Status](https://travis-ci.org/ory/ladon.svg?branch=master)](https://travis-ci.org/ory/ladon) [![Coverage Status](https://coveralls.io/repos/ory/ladon/badge.svg?branch=master&service=github)](https://coveralls.io/github/ory/ladon?branch=master) @@ -52,6 +52,7 @@ ORY builds solutions for better internet security and accessibility. We have a c - [Persistence](#persistence) - [Access Control (Warden)](#access-control-warden) - [Audit Log (Warden)](#audit-log-warden) + - [Metrics](#metrics) - [Limitations](#limitations) - [Regular expressions](#regular-expressions) - [Examples](#examples) @@ -127,7 +128,7 @@ and can answer access requests that look like: ``` However, Ladon does not come with a HTTP or server implementation. It does not restrict JSON either. We believe that it is your job to decide -if you want to use Protobuf, RESTful, HTTP, AMPQ, or some other protocol. It's up to you to write server! +if you want to use Protobuf, RESTful, HTTP, AMQP, or some other protocol. It's up to you to write the server! The following example should give you an idea what a RESTful flow *could* look like. Initially we create a policy by POSTing it to an artificial HTTP endpoint: @@ -208,7 +209,11 @@ var pol = &ladon.DefaultPolicy{ // Which resources this policy affects. // Again, you can put regular expressions in inside < >. - Resources: []string{"myrn:some.domain.com:resource:123", "myrn:some.domain.com:resource:345", "myrn:something:foo:<.+>"}, + Resources: []string{ + "myrn:some.domain.com:resource:123", "myrn:some.domain.com:resource:345", + "myrn:something:foo:<.+>", "myrn:some.domain.com:resource:<(?!protected).*>", + "myrn:some.domain.com:resource:<[[:digit:]]+>", + }, // Which actions this policy affects. Supports RegExp // Again, you can put regular expressions in inside < >. @@ -289,7 +294,7 @@ This condition is fulfilled by (we will cover the warden in the next section) ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "the-value-should-be-this", }, } @@ -300,7 +305,7 @@ but not by ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "some other value", }, } @@ -311,7 +316,7 @@ and neither by: ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "same value but other key": "the-value-should-be-this", }, } @@ -371,7 +376,7 @@ and would match in the following case: ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "the-value-should-be-this", }, } @@ -395,7 +400,7 @@ and would match in the following case: ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": true, }, }) @@ -426,7 +431,7 @@ and would match in the following case: ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "regex-pattern-here111" } } @@ -451,7 +456,7 @@ and would match var err = warden.IsAllowed(&ladon.Request{ // ... Subject: "peter", - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "peter", }, } @@ -463,7 +468,7 @@ but not: var err = warden.IsAllowed(&ladon.Request{ // ... Subject: "peter", - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": "max", }, } @@ -487,7 +492,7 @@ and would match ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": [ ["some-arbitrary-pair-value", "some-arbitrary-pair-value"], ["some-other-arbitrary-pair-value", "some-other-arbitrary-pair-value"], @@ -501,7 +506,7 @@ but not: ```go var err = warden.IsAllowed(&ladon.Request{ // ... - Context: &ladon.Context{ + Context: ladon.Context{ "some-arbitrary-key": [ ["some-arbitrary-pair-value", "some-other-arbitrary-pair-value"], ] @@ -540,7 +545,7 @@ This condition is fulfilled by this (allow for all resources containing `part:no var err = warden.IsAllowed(&ladon.Request{ // ... Resource: "rn:city:laholm:part:north" - Context: &ladon.Context{ + Context: ladon.Context{ delimiter: ":", value: "part:north" }, @@ -553,7 +558,7 @@ or ( allow all resources with `city:laholm`) var err = warden.IsAllowed(&ladon.Request{ // ... Resource: "rn:city:laholm:part:north" - Context: &ladon.Context{ + Context: ladon.Context{ delimiter: ":", value: "city:laholm" }, @@ -566,7 +571,7 @@ but not (allow for all resources containing `part:west`, the resource does not c var err = warden.IsAllowed(&ladon.Request{ // ... Resource: "rn:city:laholm:part:north" - Context: &ladon.Context{ + Context: ladon.Context{ delimiter: ":", value: "part:west" }, @@ -631,14 +636,15 @@ import "github.com/ory/ladon" func main() { // ... - if err := warden.IsAllowed(&ladon.Request{ + err := warden.IsAllowed(&ladon.Request{ Subject: "peter", Action: "delete", Resource: "myrn:some.domain.com:resource:123", Context: ladon.Context{ "ip": "127.0.0.1", }, - }); err != nil { + }) + if err != nil { log.Fatal("Access denied") } @@ -659,7 +665,7 @@ func main() { warden := ladon.Ladon{ Manager: manager.NewMemoryManager(), - AuditLogger: ladon.AuditLoggerInfo{} + AuditLogger: &ladon.AuditLoggerInfo{} } // ... @@ -668,13 +674,35 @@ func main() { It will output to `stderr` by default. +### Metrics + +Ability to track authorization grants,denials and errors, it is possible to implement own interface for processing metrics. + +```go +type prometheusMetrics struct{} + +func (mtr *prometheusMetrics) RequestDeniedBy(r ladon.Request, p ladon.Policy) {} +func (mtr *prometheusMetrics) RequestAllowedBy(r ladon.Request, policies ladon.Policies) {} +func (mtr *prometheusMetrics) RequestNoMatch(r ladon.Request) {} +func (mtr *prometheusMetrics) RequestProcessingError(r ladon.Request, err error) {} + +func main() { + + warden := ladon.Ladon{ + Manager: manager.NewMemoryManager(), + Metric: &prometheusMetrics{}, + } + + // ... +``` + ## Limitations Ladon's limitations are listed here. ### Regular expressions -Matching regular expressions has a complexity of `O(n)` and databases such as MySQL or Postgres can not +Matching regular expressions has a complexity of `O(n)` ([except](https://groups.google.com/d/msg/golang-nuts/7qgSDWPIh_E/OHTAm4wRZL0J) lookahead/lookbehind assertions) and databases such as MySQL or Postgres can not leverage indexes when parsing regular expressions. Thus, there is considerable overhead when using regular expressions. @@ -714,5 +742,5 @@ mockgen -package ladon_test -destination manager_mock_test.go github.com/ory/lad By implementing the warden.Manager it is possible to create your own adapters to persist data in a datastore of your choice. Below are a list of third party implementations. - [Redis and RethinkDB](https://github.com/ory/ladon-community) -- [CockroachDB](https://github.com/wehco/ladon-crdb) -- [sql.DB](https://github.com/wirepair/ladonsecuritymanager) +- [CockroachDB](https://github.com/dwin/ladon-crdb) +- [sql.DB](https://github.com/wirepair/ladonsqlmanager) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d1764fc --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ + + + +# Ory Security Policy + +This policy outlines Ory's security commitments and practices for users across +different licensing and deployment models. + +To learn more about Ory's security service level agreements (SLAs) and +processes, please [contact us](https://www.ory.com/contact/). + +## Ory Network Users + +- **Security SLA:** Ory addresses vulnerabilities in the Ory Network according + to the following guidelines: + - Critical: Typically addressed within 14 days. + - High: Typically addressed within 30 days. + - Medium: Typically addressed within 90 days. + - Low: Typically addressed within 180 days. + - Informational: Addressed as necessary. + These timelines are targets and may vary based on specific circumstances. +- **Release Schedule:** Updates are deployed to the Ory Network as + vulnerabilities are resolved. +- **Version Support:** The Ory Network always runs the latest version, ensuring + up-to-date security fixes. + +## Ory Enterprise License Customers + +- **Security SLA:** Ory addresses vulnerabilities based on their severity: + - Critical: Typically addressed within 14 days. + - High: Typically addressed within 30 days. + - Medium: Typically addressed within 90 days. + - Low: Typically addressed within 180 days. + - Informational: Addressed as necessary. + These timelines are targets and may vary based on specific circumstances. +- **Release Schedule:** Updates are made available as vulnerabilities are + resolved. Ory works closely with enterprise customers to ensure timely updates + that align with their operational needs. +- **Version Support:** Ory may provide security support for multiple versions, + depending on the terms of the enterprise agreement. + +## Apache 2.0 License Users + +- **Security SLA:** Ory does not provide a formal SLA for security issues under + the Apache 2.0 License. +- **Release Schedule:** Releases prioritize new functionality and include fixes + for known security vulnerabilities at the time of release. While major + releases typically occur one to two times per year, Ory does not guarantee a + fixed release schedule. +- **Version Support:** Security patches are only provided for the latest release + version. + +## Reporting a Vulnerability + +For details on how to report security vulnerabilities, visit our +[security policy documentation](https://www.ory.com/docs/ecosystem/security). diff --git a/audit_logger.go b/audit_logger.go index 0891058..991aa6e 100644 --- a/audit_logger.go +++ b/audit_logger.go @@ -20,8 +20,10 @@ package ladon +import "context" + // AuditLogger tracks denied and granted authorizations. type AuditLogger interface { - LogRejectedAccessRequest(request *Request, pool Policies, deciders Policies) - LogGrantedAccessRequest(request *Request, pool Policies, deciders Policies) + LogRejectedAccessRequest(ctx context.Context, request *Request, pool Policies, deciders Policies) + LogGrantedAccessRequest(ctx context.Context, request *Request, pool Policies, deciders Policies) } diff --git a/audit_logger_info.go b/audit_logger_info.go index 2472ef4..1360121 100644 --- a/audit_logger_info.go +++ b/audit_logger_info.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "log" "os" "strings" @@ -38,7 +39,7 @@ func (a *AuditLoggerInfo) logger() *log.Logger { return a.Logger } -func (a *AuditLoggerInfo) LogRejectedAccessRequest(r *Request, p Policies, d Policies) { +func (a *AuditLoggerInfo) LogRejectedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { if len(d) > 1 { allowed := joinPoliciesNames(d[0 : len(d)-1]) denied := d[len(d)-1].GetID() @@ -51,7 +52,7 @@ func (a *AuditLoggerInfo) LogRejectedAccessRequest(r *Request, p Policies, d Pol } } -func (a *AuditLoggerInfo) LogGrantedAccessRequest(r *Request, p Policies, d Policies) { +func (a *AuditLoggerInfo) LogGrantedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { a.logger().Printf("policies %s allow access", joinPoliciesNames(d)) } diff --git a/audit_logger_noop.go b/audit_logger_noop.go index c56878f..89fa3b1 100644 --- a/audit_logger_noop.go +++ b/audit_logger_noop.go @@ -20,10 +20,14 @@ package ladon +import "context" + // AuditLoggerNoOp is the default AuditLogger, that tracks nothing. type AuditLoggerNoOp struct{} -func (*AuditLoggerNoOp) LogRejectedAccessRequest(r *Request, p Policies, d Policies) {} -func (*AuditLoggerNoOp) LogGrantedAccessRequest(r *Request, p Policies, d Policies) {} +func (*AuditLoggerNoOp) LogRejectedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { +} +func (*AuditLoggerNoOp) LogGrantedAccessRequest(ctx context.Context, r *Request, p Policies, d Policies) { +} var DefaultAuditLogger = &AuditLoggerNoOp{} diff --git a/audit_logger_test.go b/audit_logger_test.go index 7ebdfef..c3a200d 100644 --- a/audit_logger_test.go +++ b/audit_logger_test.go @@ -22,6 +22,7 @@ package ladon_test import ( "bytes" + "context" "log" "testing" @@ -41,21 +42,23 @@ func TestAuditLogger(t *testing.T) { }, } - warden.Manager.Create(&DefaultPolicy{ + ctx := context.Background() + + warden.Manager.Create(ctx, &DefaultPolicy{ ID: "no-updates", Subjects: []string{"<.*>"}, Actions: []string{"update"}, Resources: []string{"<.*>"}, Effect: DenyAccess, }) - warden.Manager.Create(&DefaultPolicy{ + warden.Manager.Create(ctx, &DefaultPolicy{ ID: "yes-deletes", Subjects: []string{"<.*>"}, Actions: []string{"delete"}, Resources: []string{"<.*>"}, Effect: AllowAccess, }) - warden.Manager.Create(&DefaultPolicy{ + warden.Manager.Create(ctx, &DefaultPolicy{ ID: "no-bob", Subjects: []string{"bob"}, Actions: []string{"delete"}, @@ -64,7 +67,7 @@ func TestAuditLogger(t *testing.T) { }) r := &Request{} - assert.NotNil(t, warden.IsAllowed(r)) + assert.NotNil(t, warden.IsAllowed(ctx, r)) assert.Equal(t, "no policy allowed access\n", output.String()) output.Reset() @@ -72,7 +75,7 @@ func TestAuditLogger(t *testing.T) { r = &Request{ Action: "update", } - assert.NotNil(t, warden.IsAllowed(r)) + assert.NotNil(t, warden.IsAllowed(ctx, r)) assert.Equal(t, "policy no-updates forcefully denied the access\n", output.String()) output.Reset() @@ -81,7 +84,7 @@ func TestAuditLogger(t *testing.T) { Subject: "bob", Action: "delete", } - assert.NotNil(t, warden.IsAllowed(r)) + assert.NotNil(t, warden.IsAllowed(ctx, r)) assert.Equal(t, "policies yes-deletes allow access, but policy no-bob forcefully denied it\n", output.String()) output.Reset() @@ -90,6 +93,6 @@ func TestAuditLogger(t *testing.T) { Subject: "alice", Action: "delete", } - assert.Nil(t, warden.IsAllowed(r)) + assert.Nil(t, warden.IsAllowed(ctx, r)) assert.Equal(t, "policies yes-deletes allow access\n", output.String()) } diff --git a/benchmark_warden_test.go b/benchmark_warden_test.go index 572a917..bce85ef 100644 --- a/benchmark_warden_test.go +++ b/benchmark_warden_test.go @@ -21,6 +21,7 @@ package ladon_test import ( + "context" "fmt" "strconv" "testing" @@ -50,8 +51,10 @@ func benchmarkLadon(i int, b *testing.B, warden *ladon.Ladon) { // sem <- true //} + ctx := context.Background() + for _, pol := range generatePolicies(i) { - if err := warden.Manager.Create(pol); err != nil { + if err := warden.Manager.Create(ctx, pol); err != nil { b.Logf("Got error from warden.Manager.Create: %s", err) } } @@ -59,7 +62,7 @@ func benchmarkLadon(i int, b *testing.B, warden *ladon.Ladon) { b.ResetTimer() var err error for n := 0; n < b.N; n++ { - if err = warden.IsAllowed(&ladon.Request{ + if err = warden.IsAllowed(ctx, &ladon.Request{ Subject: "5", Action: "bar", Resource: "baz", diff --git a/compiler/regex.go b/compiler/regex.go index 7879cd0..2cc1db8 100644 --- a/compiler/regex.go +++ b/compiler/regex.go @@ -36,40 +36,45 @@ package compiler // Use of this source code is governed by a BSD-style // license as follows: -//Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +// Copyright (c) 2012 Rodrigo Moraes. All rights reserved. // -//Redistribution and use in source and binary forms, with or without -//modification, are permitted provided that the following conditions are -//met: +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: // -//* Redistributions of source code must retain the above copyright -//notice, this list of conditions and the following disclaimer. -//* Redistributions in binary form must reproduce the above -//copyright notice, this list of conditions and the following disclaimer -//in the documentation and/or other materials provided with the -//distribution. -//* Neither the name of Google Inc. nor the names of its -//contributors may be used to endorse or promote products derived from -//this software without specific prior written permission. +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. // -//THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -//"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -//LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -//A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -//OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -//SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -//LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -//DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -//THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -//(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -//OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import ( "bytes" "fmt" "regexp" + "time" + + "github.com/dlclark/regexp2" ) +const regexp2MatchTimeout = time.Millisecond * 250 + // delimiterIndices returns the first level delimiter indices from a string. // It returns an error in case of unbalanced delimiters. func delimiterIndices(s string, delimiterStart, delimiterEnd byte) ([]int, error) { @@ -105,18 +110,17 @@ func delimiterIndices(s string, delimiterStart, delimiterEnd byte) ([]int, error // reg, err := compiler.CompileRegex("foo:bar.baz:<[0-9]{2,10}>", '<', '>') // // if err != nil ... // reg.MatchString("foo:bar.baz:123") -func CompileRegex(tpl string, delimiterStart, delimiterEnd byte) (*regexp.Regexp, error) { +func CompileRegex(tpl string, delimiterStart, delimiterEnd byte) (*regexp2.Regexp, error) { // Check if it is well-formed. idxs, errBraces := delimiterIndices(tpl, delimiterStart, delimiterEnd) if errBraces != nil { return nil, errBraces } - varsR := make([]*regexp.Regexp, len(idxs)/2) + varsR := make([]*regexp2.Regexp, len(idxs)/2) pattern := bytes.NewBufferString("") pattern.WriteByte('^') var end int - var err error for i := 0; i < len(idxs); i += 2 { // Set all values we are interested in. raw := tpl[end:idxs[i]] @@ -125,10 +129,12 @@ func CompileRegex(tpl string, delimiterStart, delimiterEnd byte) (*regexp.Regexp // Build the regexp pattern. varIdx := i / 2 fmt.Fprintf(pattern, "%s(%s)", regexp.QuoteMeta(raw), patt) - varsR[varIdx], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + reg, err := regexp2.Compile(fmt.Sprintf("^%s$", patt), regexp2.RE2) if err != nil { return nil, err } + reg.MatchTimeout = regexp2MatchTimeout + varsR[varIdx] = reg } // Add the remaining. @@ -137,10 +143,11 @@ func CompileRegex(tpl string, delimiterStart, delimiterEnd byte) (*regexp.Regexp pattern.WriteByte('$') // Compile full regexp. - reg, errCompile := regexp.Compile(pattern.String()) + reg, errCompile := regexp2.Compile(pattern.String(), regexp2.RE2) if errCompile != nil { return nil, errCompile } + reg.MatchTimeout = regexp2MatchTimeout return reg, nil } diff --git a/compiler/regex_test.go b/compiler/regex_test.go index 23bc3e2..5c34a3b 100644 --- a/compiler/regex_test.go +++ b/compiler/regex_test.go @@ -21,9 +21,9 @@ package compiler import ( - "regexp" "testing" + "github.com/dlclark/regexp2" "github.com/stretchr/testify/assert" ) @@ -45,8 +45,18 @@ func TestRegexCompiler(t *testing.T) { {"urn:foo.bar.com:{.*{}", '{', '}', true, "", true}, {"urn:foo:<.*>", '<', '>', false, "urn:foo:bar:baz", false}, + {`urn:foo:`, '<', '>', false, "urn:foo:user=john", false}, + {`urn:foo:`, '<', '>', false, "urn:foo:user=admin", true}, + + {`urn:foo:user=<[[:digit:]]*>`, '<', '>', false, "urn:foo:user=admin", true}, + {`urn:foo:user=<[[:digit:]]*>`, '<', '>', false, "urn:foo:user=62235", false}, + + {`urn:foo:user={(?P\d{3})}`, '{', '}', false, "urn:foo:user=622", false}, + {`urn:foo:user=<(?P\d{3})>`, '<', '>', false, "urn:foo:user=622", false}, + {`urn:foo:user=<(?P\d{3})>`, '<', '>', false, "urn:foo:user=aaa", true}, + // Ignoring this case for now... - //{"urn:foo.bar.com:{.*\\{}", '{', '}', false, "", true}, + // {"urn:foo.bar.com:{.*\\{}", '{', '}', false, "", true}, } { k++ result, err := CompileRegex(c.template, c.delimiterStart, c.delimiterEnd) @@ -56,8 +66,10 @@ func TestRegexCompiler(t *testing.T) { } t.Logf("Case %d compiled to: %s", k, result.String()) - ok, err := regexp.MatchString(result.String(), c.matchAgainst) + re := regexp2.MustCompile(result.String(), regexp2.RE2) + ok, err := re.MatchString(c.matchAgainst) assert.Nil(t, err, "Case %d", k) assert.Equal(t, !c.failMatch, ok, "Case %d", k) + } } diff --git a/condition.go b/condition.go index b653359..fc161b1 100644 --- a/condition.go +++ b/condition.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "encoding/json" "github.com/pkg/errors" @@ -32,7 +33,7 @@ type Condition interface { GetName() string // Fulfills returns true if the request is fulfilled by the condition. - Fulfills(interface{}, *Request) bool + Fulfills(context.Context, interface{}, *Request) bool } // Conditions is a collection of conditions. @@ -128,4 +129,7 @@ var ConditionFactories = map[string]func() Condition{ new(ResourceContainsCondition).GetName(): func() Condition { return new(ResourceContainsCondition) }, + new(BooleanCondition).GetName(): func() Condition { + return new(BooleanCondition) + }, } diff --git a/condition_boolean.go b/condition_boolean.go index eb04c1f..acd0f75 100644 --- a/condition_boolean.go +++ b/condition_boolean.go @@ -1,5 +1,7 @@ package ladon +import "context" + /* BooleanCondition is used to determine if a boolean context matches an expected boolean condition. @@ -18,7 +20,7 @@ func (c *BooleanCondition) GetName() string { // Fulfills determines if the BooleanCondition is fulfilled. // The BooleanCondition is fulfilled if the provided boolean value matches the conditions boolean value. -func (c *BooleanCondition) Fulfills(value interface{}, _ *Request) bool { +func (c *BooleanCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { val, ok := value.(bool) return ok && val == c.BooleanValue diff --git a/condition_cidr.go b/condition_cidr.go index 3a3ce53..85d2e86 100644 --- a/condition_cidr.go +++ b/condition_cidr.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "net" ) @@ -30,7 +31,7 @@ type CIDRCondition struct { } // Fulfills returns true if the the request is fulfilled by the condition. -func (c *CIDRCondition) Fulfills(value interface{}, _ *Request) bool { +func (c *CIDRCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { ips, ok := value.(string) if !ok { return false diff --git a/condition_cidr_test.go b/condition_cidr_test.go index afbb151..c2e5989 100644 --- a/condition_cidr_test.go +++ b/condition_cidr_test.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -42,6 +43,6 @@ func TestCIDRMatch(t *testing.T) { CIDR: c.cidr, } - assert.Equal(t, c.pass, condition.Fulfills(c.ip, new(Request)), "%s; %s", c.ip, c.cidr) + assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.ip, new(Request)), "%s; %s", c.ip, c.cidr) } } diff --git a/condition_resource_contains.go b/condition_resource_contains.go index 7ac1040..c3b98b4 100644 --- a/condition_resource_contains.go +++ b/condition_resource_contains.go @@ -20,13 +20,16 @@ package ladon -import "strings" +import ( + "context" + "strings" +) // ResourceContainsCondition is fulfilled if the context matches a substring within the resource name type ResourceContainsCondition struct{} // Fulfills returns true if the request's resouce contains the given value string -func (c *ResourceContainsCondition) Fulfills(value interface{}, r *Request) bool { +func (c *ResourceContainsCondition) Fulfills(ctx context.Context, value interface{}, r *Request) bool { filter, ok := value.(map[string]interface{}) if !ok { diff --git a/condition_resource_contains_test.go b/condition_resource_contains_test.go index 74cca88..50b2369 100644 --- a/condition_resource_contains_test.go +++ b/condition_resource_contains_test.go @@ -33,6 +33,7 @@ package ladon import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -82,7 +83,7 @@ func TestResourceContains(t *testing.T) { } resourceFilter["value"] = c.value - assert.Equal(t, c.pass, condition.Fulfills(resourceFilter, request), "%s", c.matches) + assert.Equal(t, c.pass, condition.Fulfills(context.Background(), resourceFilter, request), "%s", c.matches) assert.Equal(t, condition.GetName(), "ResourceContainsCondition", "should be called ResourceContainsCondition") } } diff --git a/condition_string_equal.go b/condition_string_equal.go index c4ecee2..15686bc 100644 --- a/condition_string_equal.go +++ b/condition_string_equal.go @@ -20,6 +20,8 @@ package ladon +import "context" + // StringEqualCondition is a condition which is fulfilled if the given // string value is the same as specified in StringEqualCondition type StringEqualCondition struct { @@ -28,7 +30,7 @@ type StringEqualCondition struct { // Fulfills returns true if the given value is a string and is the // same as in StringEqualCondition.Equals -func (c *StringEqualCondition) Fulfills(value interface{}, _ *Request) bool { +func (c *StringEqualCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { s, ok := value.(string) return ok && s == c.Equals diff --git a/condition_string_match.go b/condition_string_match.go index bb42b97..61cd4cd 100644 --- a/condition_string_match.go +++ b/condition_string_match.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "regexp" ) @@ -32,7 +33,7 @@ type StringMatchCondition struct { // Fulfills returns true if the given value is a string and matches the regex // pattern in StringMatchCondition.Matches -func (c *StringMatchCondition) Fulfills(value interface{}, _ *Request) bool { +func (c *StringMatchCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { s, ok := value.(string) matches, _ := regexp.MatchString(c.Matches, s) diff --git a/condition_string_match_test.go b/condition_string_match_test.go index 7920c43..ac222d9 100644 --- a/condition_string_match_test.go +++ b/condition_string_match_test.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -40,6 +41,6 @@ func TestStringMatch(t *testing.T) { Matches: c.matches, } - assert.Equal(t, c.pass, condition.Fulfills(c.value, new(Request)), "%s", c.matches) + assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.value, new(Request)), "%s", c.matches) } } diff --git a/condition_string_pairs_equal.go b/condition_string_pairs_equal.go index b99ac29..6eee7d2 100644 --- a/condition_string_pairs_equal.go +++ b/condition_string_pairs_equal.go @@ -20,6 +20,8 @@ package ladon +import "context" + // StringPairsEqualCondition is a condition which is fulfilled if the given // array of pairs contains two-element string arrays where both elements // in the string array are equal @@ -27,7 +29,7 @@ type StringPairsEqualCondition struct{} // Fulfills returns true if the given value is an array of string arrays and // each string array has exactly two values which are equal -func (c *StringPairsEqualCondition) Fulfills(value interface{}, _ *Request) bool { +func (c *StringPairsEqualCondition) Fulfills(ctx context.Context, value interface{}, _ *Request) bool { pairs, PairsOk := value.([]interface{}) if !PairsOk { return false diff --git a/condition_string_pairs_equal_test.go b/condition_string_pairs_equal_test.go index a609bd4..4dda59d 100644 --- a/condition_string_pairs_equal_test.go +++ b/condition_string_pairs_equal_test.go @@ -21,6 +21,7 @@ package ladon import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -43,6 +44,6 @@ func TestStringPairsEqualMatch(t *testing.T) { } { condition := &StringPairsEqualCondition{} - assert.Equal(t, c.pass, condition.Fulfills(c.pairs, new(Request)), "%s", c.pairs) + assert.Equal(t, c.pass, condition.Fulfills(context.Background(), c.pairs, new(Request)), "%s", c.pairs) } } diff --git a/condition_subject_equal.go b/condition_subject_equal.go index e58ed18..aa10236 100644 --- a/condition_subject_equal.go +++ b/condition_subject_equal.go @@ -20,11 +20,13 @@ package ladon +import "context" + // EqualsSubjectCondition is a condition which is fulfilled if the request's subject is equal to the given value string type EqualsSubjectCondition struct{} // Fulfills returns true if the request's subject is equal to the given value string -func (c *EqualsSubjectCondition) Fulfills(value interface{}, r *Request) bool { +func (c *EqualsSubjectCondition) Fulfills(ctx context.Context, value interface{}, r *Request) bool { s, ok := value.(string) return ok && s == r.Subject diff --git a/condition_test.go b/condition_test.go index 49449c2..e2f1ba0 100644 --- a/condition_test.go +++ b/condition_test.go @@ -50,9 +50,10 @@ func TestMarshalUnmarshalNative(t *testing.T) { func TestMarshalUnmarshal(t *testing.T) { css := &Conditions{ - "clientIP": &CIDRCondition{CIDR: "127.0.0.1/0"}, - "owner": &EqualsSubjectCondition{}, - "role": &StringMatchCondition{Matches: ".*"}, + "clientIP": &CIDRCondition{CIDR: "127.0.0.1/0"}, + "owner": &EqualsSubjectCondition{}, + "role": &StringMatchCondition{Matches: ".*"}, + "hasElevatedPrivileges": &BooleanCondition{BooleanValue: true}, } out, err := json.Marshal(css) require.Nil(t, err) @@ -75,15 +76,22 @@ func TestMarshalUnmarshal(t *testing.T) { "matches": ".*" } }, + "hasElevatedPrivileges": { + "type": "BooleanCondition", + "options": { + "value": true + } + }, "resourceFilter": { "type": "ResourceContainsCondition" } }`), &cs)) - require.Len(t, cs, 4) + require.Len(t, cs, 5) assert.IsType(t, &EqualsSubjectCondition{}, cs["owner"]) assert.IsType(t, &CIDRCondition{}, cs["clientIP"]) assert.IsType(t, &StringMatchCondition{}, cs["role"]) + assert.IsType(t, &BooleanCondition{}, cs["hasElevatedPrivileges"]) assert.IsType(t, &ResourceContainsCondition{}, cs["resourceFilter"]) } diff --git a/docs/images/banner_ladon.png b/docs/images/banner_ladon.png index 8b8c7ce..ac54cd0 100644 Binary files a/docs/images/banner_ladon.png and b/docs/images/banner_ladon.png differ diff --git a/go.mod b/go.mod index b776e73..3dcc6d2 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,19 @@ module github.com/ory/ladon +go 1.19 + require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/golang/mock v1.1.1 + github.com/dlclark/regexp2 v1.2.0 + github.com/golang/mock v1.6.0 github.com/hashicorp/golang-lru v0.5.0 github.com/ory/pagination v0.0.1 github.com/pborman/uuid v1.2.0 github.com/pkg/errors v0.8.0 - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 - golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 // indirect +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/uuid v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index a1cf9bf..119186f 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= @@ -16,5 +18,26 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ladon.go b/ladon.go index cf6e7ae..995ee93 100644 --- a/ladon.go +++ b/ladon.go @@ -21,6 +21,8 @@ package ladon import ( + "context" + "github.com/pkg/errors" ) @@ -29,47 +31,58 @@ type Ladon struct { Manager Manager Matcher matcher AuditLogger AuditLogger + Metric Metric } func (l *Ladon) matcher() matcher { - if l.Matcher == nil { - l.Matcher = DefaultMatcher + if l.Matcher != nil { + return l.Matcher } - return l.Matcher + return DefaultMatcher } func (l *Ladon) auditLogger() AuditLogger { - if l.AuditLogger == nil { - l.AuditLogger = DefaultAuditLogger + if l.AuditLogger != nil { + return l.AuditLogger + } + return DefaultAuditLogger +} + +func (l *Ladon) metric() Metric { + if l.Metric == nil { + l.Metric = DefaultMetric } - return l.AuditLogger + return l.Metric } // IsAllowed returns nil if subject s has permission p on resource r with context c or an error otherwise. -func (l *Ladon) IsAllowed(r *Request) (err error) { - policies, err := l.Manager.FindRequestCandidates(r) +func (l *Ladon) IsAllowed(ctx context.Context, r *Request) (err error) { + policies, err := l.Manager.FindRequestCandidates(ctx, r) if err != nil { + go l.metric().RequestProcessingError(*r, nil, err) return err } // Although the manager is responsible of matching the policies, it might decide to just scan for // subjects, it might return all policies, or it might have a different pattern matching than Golang. // Thus, we need to make sure that we actually matched the right policies. - return l.DoPoliciesAllow(r, policies) + return l.DoPoliciesAllow(ctx, r, policies) } // DoPoliciesAllow returns nil if subject s has permission p on resource r with context c for a given policy list or an error otherwise. // 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. -func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { +func (l *Ladon) DoPoliciesAllow(ctx context.Context, r *Request, policies []Policy) (err error) { var allowed = false var deciders = Policies{} // Iterate through all policies for _, p := range policies { + // Does the action match with one of the policies? // This is the first check because usually actions are a superset of get|update|delete|set // and thus match faster. if pm, err := l.matcher().Matches(p, p.GetActions(), r.Action); err != nil { + go l.metric().RequestProcessingError(*r, p, err) return errors.WithStack(err) } else if !pm { // no, continue to next policy @@ -80,6 +93,7 @@ func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { // There are usually less subjects than resources which is why this is checked // before checking for resources. if sm, err := l.matcher().Matches(p, p.GetSubjects(), r.Subject); err != nil { + go l.metric().RequestProcessingError(*r, p, err) return err } else if !sm { // no, continue to next policy @@ -88,6 +102,7 @@ func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { // Does the resource match with one of the policies? if rm, err := l.matcher().Matches(p, p.GetResources(), r.Resource); err != nil { + go l.metric().RequestProcessingError(*r, p, err) return errors.WithStack(err) } else if !rm { // no, continue to next policy @@ -96,15 +111,16 @@ func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { // Are the policies conditions met? // This is checked first because it usually has a small complexity. - if !l.passesConditions(p, r) { + if !l.passesConditions(ctx, p, r) { // no, continue to next policy continue } - // Is the policies effect deny? If yes, this overrides all allow policies -> access denied. + // Is the policy's effect `deny`? If yes, this overrides all allow policies -> access denied. if !p.AllowAccess() { deciders = append(deciders, p) - l.auditLogger().LogRejectedAccessRequest(r, policies, deciders) + l.auditLogger().LogRejectedAccessRequest(ctx, r, policies, deciders) + go l.metric().RequestDeniedBy(*r, p) return errors.WithStack(ErrRequestForcefullyDenied) } @@ -113,17 +129,21 @@ func (l *Ladon) DoPoliciesAllow(r *Request, policies []Policy) (err error) { } if !allowed { - l.auditLogger().LogRejectedAccessRequest(r, policies, deciders) + go l.metric().RequestNoMatch(*r) + + l.auditLogger().LogRejectedAccessRequest(ctx, r, policies, deciders) return errors.WithStack(ErrRequestDenied) } - l.auditLogger().LogGrantedAccessRequest(r, policies, deciders) + l.auditLogger().LogGrantedAccessRequest(ctx, r, policies, deciders) + l.metric().RequestAllowedBy(*r, deciders) + return nil } -func (l *Ladon) passesConditions(p Policy, r *Request) bool { +func (l *Ladon) passesConditions(ctx context.Context, p Policy, r *Request) bool { for key, condition := range p.GetConditions() { - if pass := condition.Fulfills(r.Context[key], r); !pass { + if pass := condition.Fulfills(ctx, r.Context[key], r); !pass { return false } } diff --git a/ladon_test.go b/ladon_test.go index bfe2e37..353e64f 100644 --- a/ladon_test.go +++ b/ladon_test.go @@ -21,6 +21,7 @@ package ladon_test import ( + "context" "fmt" "testing" @@ -34,7 +35,7 @@ import ( // A bunch of exemplary policies var pols = []Policy{ &DefaultPolicy{ - ID: "1", + ID: "0", Description: `This policy allows max, peter, zac and ken to create, delete and get the listed resources, but only if the client ip matches and the request states that they are the owner of those resources as well.`, Subjects: []string{"max", "peter", ""}, @@ -49,7 +50,7 @@ var pols = []Policy{ }, }, &DefaultPolicy{ - ID: "2", + ID: "1", Description: "This policy allows max to update any resource", Subjects: []string{"max"}, Actions: []string{"update"}, @@ -64,6 +65,30 @@ var pols = []Policy{ Resources: []string{"<.*>"}, Effect: DenyAccess, }, + &DefaultPolicy{ + ID: "2", + Description: "This policy denies max to broadcast any of the resources", + Subjects: []string{"max"}, + Actions: []string{"random"}, + Resources: []string{"<.*>"}, + Effect: DenyAccess, + }, + &DefaultPolicy{ + ID: "4", + Description: "This policy allows swen to update any resource except `protected` resources", + Subjects: []string{"swen"}, + Actions: []string{"update"}, + Resources: []string{"myrn:some.domain.com:resource:<(?!protected).*>"}, + Effect: AllowAccess, + }, + &DefaultPolicy{ + ID: "5", + Description: "This policy allows richard to update resources which names consists of digits only", + Subjects: []string{"richard"}, + Actions: []string{"update"}, + Resources: []string{"myrn:some.domain.com:resource:<[[:digit:]]+>"}, + Effect: AllowAccess, + }, } // Some test cases @@ -146,30 +171,82 @@ var cases = []struct { }, expectErr: true, }, + { + description: "should pass because swen is allowed to update all resources except `protected` resources.", + accessRequest: &Request{ + Subject: "swen", + Action: "update", + Resource: "myrn:some.domain.com:resource:123", + }, + expectErr: false, + }, + { + description: "should fail because swen is not allowed to update `protected` resource", + accessRequest: &Request{ + Subject: "swen", + Action: "update", + Resource: "myrn:some.domain.com:resource:protected123", + }, + expectErr: true, + }, + { + description: "should fail because richard is not allowed to update a resource with alphanumeric name", + accessRequest: &Request{ + Subject: "richard", + Action: "update", + Resource: "myrn:some.domain.com:resource:protected123", + }, + expectErr: true, + }, + { + description: "should pass because richard is allowed to update a resources with a name containing digits only", + accessRequest: &Request{ + Subject: "richard", + Action: "update", + Resource: "myrn:some.domain.com:resource:25222", + }, + expectErr: false, + }, } func TestLadon(t *testing.T) { + + ctx := context.Background() + // Instantiate ladon with the default in-memory store. warden := &Ladon{Manager: NewMemoryManager()} // Add the policies defined above to the memory manager. for _, pol := range pols { - require.Nil(t, warden.Manager.Create(pol)) + require.Nil(t, warden.Manager.Create(ctx, pol)) + } + + for i := 0; i < len(pols); i++ { + polices, err := warden.Manager.GetAll(ctx, int64(1), int64(i)) + require.NoError(t, err) + p, err := warden.Manager.Get(ctx, fmt.Sprintf("%d", i)) + if err == nil { + AssertPolicyEqual(t, p, polices[0]) + } } for k, c := range cases { t.Run(fmt.Sprintf("case=%d-%s", k, c.description), func(t *testing.T) { // This is where we ask the warden if the access requests should be granted - err := warden.IsAllowed(c.accessRequest) + err := warden.IsAllowed(ctx, c.accessRequest) assert.Equal(t, c.expectErr, err != nil) }) } + } func TestLadonEmpty(t *testing.T) { + + ctx := context.Background() + // If no policy was given, the warden must return an error! warden := &Ladon{Manager: NewMemoryManager()} - assert.NotNil(t, warden.IsAllowed(&Request{})) + assert.NotNil(t, warden.IsAllowed(ctx, &Request{})) } diff --git a/manager.go b/manager.go index d775ffe..93d5936 100644 --- a/manager.go +++ b/manager.go @@ -20,36 +20,38 @@ package ladon +import "context" + // Manager is responsible for managing and persisting policies. type Manager interface { // Create persists the policy. - Create(policy Policy) error + Create(ctx context.Context, policy Policy) error // Update updates an existing policy. - Update(policy Policy) error + Update(ctx context.Context, policy Policy) error // Get retrieves a policy. - Get(id string) (Policy, error) + Get(ctx context.Context, id string) (Policy, error) // Delete removes a policy. - Delete(id string) error + Delete(ctx context.Context, id string) error // GetAll retrieves all policies. - GetAll(limit, offset int64) (Policies, error) + GetAll(ctx context.Context, limit, offset int64) (Policies, error) // FindRequestCandidates returns candidates that could match the request object. It either returns // a set that exactly matches the request, or a superset of it. If an error occurs, it returns nil and // the error. - FindRequestCandidates(r *Request) (Policies, error) + FindRequestCandidates(ctx context.Context, r *Request) (Policies, error) // FindPoliciesForSubject returns policies that could match the subject. It either returns // a set of policies that applies to the subject, or a superset of it. // If an error occurs, it returns nil and the error. - FindPoliciesForSubject(subject string) (Policies, error) + FindPoliciesForSubject(ctx context.Context, subject string) (Policies, error) // FindPoliciesForResource returns policies that could match the resource. It either returns // a set of policies that apply to the resource, or a superset of it. // If an error occurs, it returns nil and the error. - FindPoliciesForResource(resource string) (Policies, error) + FindPoliciesForResource(ctx context.Context, resource string) (Policies, error) } diff --git a/manager/memory/manager_memory.go b/manager/memory/manager_memory.go index 1cb6c89..3c85f6a 100644 --- a/manager/memory/manager_memory.go +++ b/manager/memory/manager_memory.go @@ -21,6 +21,8 @@ package memory import ( + "context" + "sort" "sync" "github.com/pkg/errors" @@ -43,7 +45,7 @@ func NewMemoryManager() *MemoryManager { } // Update updates an existing policy. -func (m *MemoryManager) Update(policy Policy) error { +func (m *MemoryManager) Update(ctx context.Context, policy Policy) error { m.Lock() defer m.Unlock() m.Policies[policy.GetID()] = policy @@ -51,21 +53,29 @@ func (m *MemoryManager) Update(policy Policy) error { } // GetAll returns all policies. -func (m *MemoryManager) GetAll(limit, offset int64) (Policies, error) { - ps := make(Policies, len(m.Policies)) +func (m *MemoryManager) GetAll(ctx context.Context, limit, offset int64) (Policies, error) { + keys := make([]string, len(m.Policies)) i := 0 - - for _, p := range m.Policies { - ps[i] = p + m.RLock() + for key := range m.Policies { + keys[i] = key i++ } - start, end := pagination.Index(int(limit), int(offset), len(ps)) - return ps[start:end], nil + start, end := pagination.Index(int(limit), int(offset), len(m.Policies)) + sort.Strings(keys) + ps := make(Policies, len(keys[start:end])) + i = 0 + for _, key := range keys[start:end] { + ps[i] = m.Policies[key] + i++ + } + m.RUnlock() + return ps, nil } // Create a new pollicy to MemoryManager. -func (m *MemoryManager) Create(policy Policy) error { +func (m *MemoryManager) Create(ctx context.Context, policy Policy) error { m.Lock() defer m.Unlock() @@ -78,7 +88,7 @@ func (m *MemoryManager) Create(policy Policy) error { } // Get retrieves a policy. -func (m *MemoryManager) Get(id string) (Policy, error) { +func (m *MemoryManager) Get(ctx context.Context, id string) (Policy, error) { m.RLock() defer m.RUnlock() p, ok := m.Policies[id] @@ -90,7 +100,7 @@ func (m *MemoryManager) Get(id string) (Policy, error) { } // Delete removes a policy. -func (m *MemoryManager) Delete(id string) error { +func (m *MemoryManager) Delete(ctx context.Context, id string) error { m.Lock() defer m.Unlock() delete(m.Policies, id) @@ -112,20 +122,20 @@ func (m *MemoryManager) findAllPolicies() (Policies, error) { // FindRequestCandidates returns candidates that could match the request object. It either returns // a set that exactly matches the request, or a superset of it. If an error occurs, it returns nil and // the error. -func (m *MemoryManager) FindRequestCandidates(r *Request) (Policies, error) { +func (m *MemoryManager) FindRequestCandidates(ctx context.Context, r *Request) (Policies, error) { return m.findAllPolicies() } // FindPoliciesForSubject returns policies that could match the subject. It either returns // a set of policies that applies to the subject, or a superset of it. // If an error occurs, it returns nil and the error. -func (m *MemoryManager) FindPoliciesForSubject(subject string) (Policies, error) { +func (m *MemoryManager) FindPoliciesForSubject(ctx context.Context, subject string) (Policies, error) { return m.findAllPolicies() } // FindPoliciesForResource returns policies that could match the resource. It either returns // a set of policies that apply to the resource, or a superset of it. // If an error occurs, it returns nil and the error. -func (m *MemoryManager) FindPoliciesForResource(resource string) (Policies, error) { +func (m *MemoryManager) FindPoliciesForResource(ctx context.Context, resource string) (Policies, error) { return m.findAllPolicies() } diff --git a/manager_all_test.go b/manager_all_test.go index aaf9e72..b0dc7b4 100644 --- a/manager_all_test.go +++ b/manager_all_test.go @@ -41,13 +41,13 @@ func connectMEM() { func TestManagers(t *testing.T) { t.Run("type=get errors", func(t *testing.T) { for k, s := range managers { - t.Run("manager="+k, TestHelperGetErrors(s)) + t.Run("manager="+k, HelperTestGetErrors(s)) } }) t.Run("type=CRUD", func(t *testing.T) { for k, s := range managers { - t.Run(fmt.Sprintf("manager=%s", k), TestHelperCreateGetDelete(s)) + t.Run(fmt.Sprintf("manager=%s", k), HelperTestCreateGetDelete(s)) } }) @@ -56,8 +56,8 @@ func TestManagers(t *testing.T) { "postgres": managers["postgres"], "mysql": managers["mysql"], } { - t.Run(fmt.Sprintf("manager=%s", k), TestHelperFindPoliciesForSubject(k, s)) - t.Run(fmt.Sprintf("manager=%s", k), TestHelperFindPoliciesForResource(k, s)) + t.Run(fmt.Sprintf("manager=%s", k), HelperTestFindPoliciesForSubject(k, s)) + t.Run(fmt.Sprintf("manager=%s", k), HelperTestFindPoliciesForResource(k, s)) } }) } diff --git a/manager_test_helper.go b/manager_helper_test.go similarity index 73% rename from manager_test_helper.go rename to manager_helper_test.go index cde1dad..860a021 100644 --- a/manager_test_helper.go +++ b/manager_helper_test.go @@ -18,140 +18,142 @@ * @license Apache-2.0 */ -package ladon +package ladon_test import ( + "context" "fmt" "reflect" "testing" + "github.com/ory/ladon" "github.com/pborman/uuid" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -var TestManagerPolicies = []*DefaultPolicy{ +var TestManagerPolicies = []*ladon.DefaultPolicy{ { ID: uuid.New(), Description: "description", Subjects: []string{"user", "anonymous"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"article", "user"}, Actions: []string{"create", "update"}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{""}, Actions: []string{"view"}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{}, Actions: []string{"view"}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{}, Actions: []string{}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"foo"}, Actions: []string{}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{"foo"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"foo"}, Actions: []string{}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{"foo"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{}, Actions: []string{}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", - Effect: AllowAccess, - Conditions: Conditions{}, + Effect: ladon.AllowAccess, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "description", Subjects: []string{""}, - Effect: DenyAccess, + Effect: ladon.DenyAccess, Resources: []string{"article", "user"}, Actions: []string{"view"}, - Conditions: Conditions{ - "owner": &EqualsSubjectCondition{}, + Conditions: ladon.Conditions{ + "owner": &ladon.EqualsSubjectCondition{}, }, }, { ID: uuid.New(), Description: "description", Subjects: []string{"", "peter"}, - Effect: DenyAccess, + Effect: ladon.DenyAccess, Resources: []string{".*"}, Actions: []string{"disable"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ + Conditions: ladon.Conditions{ + "ip": &ladon.CIDRCondition{ CIDR: "1234", }, - "owner": &EqualsSubjectCondition{}, + "owner": &ladon.EqualsSubjectCondition{}, }, }, { ID: uuid.New(), Description: "description", Subjects: []string{"<.*>"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{""}, Actions: []string{"view"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ + Conditions: ladon.Conditions{ + "ip": &ladon.CIDRCondition{ CIDR: "1234", }, - "owner": &EqualsSubjectCondition{}, + "owner": &ladon.EqualsSubjectCondition{}, }, }, { ID: uuid.New(), Description: "description", Subjects: []string{""}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{""}, Actions: []string{"view"}, - Conditions: Conditions{ - "ip": &CIDRCondition{ + Conditions: ladon.Conditions{ + "ip": &ladon.CIDRCondition{ CIDR: "1234", }, - "owner": &EqualsSubjectCondition{}, + "owner": &ladon.EqualsSubjectCondition{}, }, }, //Two new policies which do not persist in MySQL correctly @@ -159,32 +161,32 @@ var TestManagerPolicies = []*DefaultPolicy{ ID: uuid.New(), Description: "A failed policy", Subjects: []string{"supplier"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"product:<.*>"}, Actions: []string{"update"}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, { ID: uuid.New(), Description: "Another failed policy", Subjects: []string{"buyer"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"products:attributeGroup:<.*>"}, Actions: []string{"create"}, - Conditions: Conditions{}, + Conditions: ladon.Conditions{}, }, } -var testPolicies = []*DefaultPolicy{ +var testPolicies = []*ladon.DefaultPolicy{ { ID: uuid.New(), Description: "description", Subjects: []string{"sql<.*>match"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"master", "user", "article"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, @@ -193,11 +195,11 @@ var testPolicies = []*DefaultPolicy{ ID: uuid.New(), Description: "description", Subjects: []string{"sqlmatch"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"master", "user", "article"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, @@ -206,11 +208,11 @@ var testPolicies = []*DefaultPolicy{ ID: uuid.New(), Description: "description", Subjects: []string{}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"master", "user", "article"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, @@ -218,11 +220,11 @@ var testPolicies = []*DefaultPolicy{ { ID: uuid.New(), Description: "description", - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"master", "user", "article"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, @@ -231,11 +233,11 @@ var testPolicies = []*DefaultPolicy{ ID: uuid.New(), Description: "description", Subjects: []string{"some"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"sqlmatch_resource"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, @@ -244,26 +246,28 @@ var testPolicies = []*DefaultPolicy{ ID: uuid.New(), Description: "description", Subjects: []string{"other"}, - Effect: AllowAccess, + Effect: ladon.AllowAccess, Resources: []string{"sql<.*>resource"}, Actions: []string{"create", "update", "delete"}, - Conditions: Conditions{ - "foo": &StringEqualCondition{ + Conditions: ladon.Conditions{ + "foo": &ladon.StringEqualCondition{ Equals: "foo", }, }, }, } -func TestHelperFindPoliciesForSubject(k string, s Manager) func(t *testing.T) { +func HelperTestFindPoliciesForSubject(k string, s ladon.Manager) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { for _, c := range testPolicies { t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { - require.NoError(t, s.Create(c)) + require.NoError(t, s.Create(ctx, c)) }) } - res, err := s.FindRequestCandidates(&Request{ + res, err := s.FindRequestCandidates(ctx, &ladon.Request{ Subject: "sqlmatch", Resource: "article", Action: "create", @@ -279,7 +283,7 @@ func TestHelperFindPoliciesForSubject(k string, s Manager) func(t *testing.T) { AssertPolicyEqual(t, testPolicies[1], res[0]) } - res, err = s.FindRequestCandidates(&Request{ + res, err = s.FindRequestCandidates(ctx, &ladon.Request{ Subject: "sqlamatch", Resource: "article", Action: "create", @@ -291,15 +295,17 @@ func TestHelperFindPoliciesForSubject(k string, s Manager) func(t *testing.T) { } } -func TestHelperFindPoliciesForResource(k string, s Manager) func(t *testing.T) { +func HelperTestFindPoliciesForResource(k string, s ladon.Manager) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { for _, c := range testPolicies { t.Run(fmt.Sprintf("create=%s", k), func(t *testing.T) { - require.NoError(t, s.Create(c)) + require.NoError(t, s.Create(ctx, c)) }) } - res, err := s.FindPoliciesForResource("sqlmatch_resource") + res, err := s.FindPoliciesForResource(ctx, "sqlmatch_resource") require.NoError(t, err) require.Len(t, res, 2) @@ -311,7 +317,7 @@ func TestHelperFindPoliciesForResource(k string, s Manager) func(t *testing.T) { AssertPolicyEqual(t, testPolicies[len(testPolicies)-1], res[0]) } - res, err = s.FindPoliciesForResource("sqlamatch_resource") + res, err = s.FindPoliciesForResource(ctx, "sqlamatch_resource") require.NoError(t, err) require.Len(t, res, 1) @@ -319,7 +325,7 @@ func TestHelperFindPoliciesForResource(k string, s Manager) func(t *testing.T) { } } -func AssertPolicyEqual(t *testing.T, expected, got Policy) { +func AssertPolicyEqual(t *testing.T, expected, got ladon.Policy) { assert.Equal(t, expected.GetID(), got.GetID()) assert.Equal(t, expected.GetDescription(), got.GetDescription()) assert.Equal(t, expected.GetEffect(), got.GetEffect()) @@ -368,27 +374,31 @@ func testEq(a, b []string) error { return nil } -func TestHelperGetErrors(s Manager) func(t *testing.T) { +func HelperTestGetErrors(s ladon.Manager) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { - _, err := s.Get(uuid.New()) + _, err := s.Get(ctx, uuid.New()) assert.Error(t, err) - _, err = s.Get("asdf") + _, err = s.Get(ctx, "asdf") assert.Error(t, err) } } -func TestHelperCreateGetDelete(s Manager) func(t *testing.T) { +func HelperTestCreateGetDelete(s ladon.Manager) func(t *testing.T) { + ctx := context.Background() + return func(t *testing.T) { for i, c := range TestManagerPolicies { t.Run(fmt.Sprintf("case=%d/id=%s/type=create", i, c.GetID()), func(t *testing.T) { - _, err := s.Get(c.GetID()) + _, err := s.Get(ctx, c.GetID()) require.Error(t, err) - require.NoError(t, s.Create(c)) + require.NoError(t, s.Create(ctx, c)) }) t.Run(fmt.Sprintf("case=%d/id=%s/type=query", i, c.GetID()), func(t *testing.T) { - get, err := s.Get(c.GetID()) + get, err := s.Get(ctx, c.GetID()) require.NoError(t, err) AssertPolicyEqual(t, c, get) @@ -396,16 +406,16 @@ func TestHelperCreateGetDelete(s Manager) func(t *testing.T) { t.Run(fmt.Sprintf("case=%d/id=%s/type=update", i, c.GetID()), func(t *testing.T) { c.Description = c.Description + "_updated" - require.NoError(t, s.Update(c)) + require.NoError(t, s.Update(ctx, c)) - get, err := s.Get(c.GetID()) + get, err := s.Get(ctx, c.GetID()) require.NoError(t, err) AssertPolicyEqual(t, c, get) }) t.Run(fmt.Sprintf("case=%d/id=%s/type=query", i, c.GetID()), func(t *testing.T) { - get, err := s.Get(c.GetID()) + get, err := s.Get(ctx, c.GetID()) require.NoError(t, err) AssertPolicyEqual(t, c, get) @@ -415,19 +425,19 @@ func TestHelperCreateGetDelete(s Manager) func(t *testing.T) { t.Run("type=query-all", func(t *testing.T) { count := int64(len(TestManagerPolicies)) - pols, err := s.GetAll(100, 0) + pols, err := s.GetAll(ctx, 100, 0) require.NoError(t, err) assert.Len(t, pols, len(TestManagerPolicies)) - pols4, err := s.GetAll(1, 0) + pols4, err := s.GetAll(ctx, 1, 0) require.NoError(t, err) assert.Len(t, pols4, 1) - pols2, err := s.GetAll(100, count-1) + pols2, err := s.GetAll(ctx, 100, count-1) require.NoError(t, err) assert.Len(t, pols2, 1) - pols3, err := s.GetAll(100, count) + pols3, err := s.GetAll(ctx, 100, count) require.NoError(t, err) assert.Len(t, pols3, 0) @@ -458,9 +468,9 @@ func TestHelperCreateGetDelete(s Manager) func(t *testing.T) { for i, c := range TestManagerPolicies { t.Run(fmt.Sprintf("case=%d/id=%s/type=delete", i, c.GetID()), func(t *testing.T) { - assert.NoError(t, s.Delete(c.ID)) + assert.NoError(t, s.Delete(ctx, c.ID)) - _, err := s.Get(c.GetID()) + _, err := s.Get(ctx, c.GetID()) assert.Error(t, err) }) } diff --git a/manager_mock_test.go b/manager_mock_test.go index 969d83c..e5bcd08 100644 --- a/manager_mock_test.go +++ b/manager_mock_test.go @@ -1,136 +1,153 @@ -/* - * Copyright © 2016-2018 Aeneas Rekkas - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @author Aeneas Rekkas - * @copyright 2015-2018 Aeneas Rekkas - * @license Apache-2.0 - */ - -// Automatically generated by MockGen. DO NOT EDIT! +// Code generated by MockGen. DO NOT EDIT. // Source: github.com/ory/ladon (interfaces: Manager) +// Package ladon_test is a generated GoMock package. package ladon_test import ( - gomock "github.com/golang/mock/gomock" + context "context" + reflect "reflect" + gomock "github.com/golang/mock/gomock" ladon "github.com/ory/ladon" ) -// Mock of Manager interface +// MockManager is a mock of Manager interface. type MockManager struct { ctrl *gomock.Controller - recorder *_MockManagerRecorder + recorder *MockManagerMockRecorder } -// Recorder for MockManager (not exported) -type _MockManagerRecorder struct { +// MockManagerMockRecorder is the mock recorder for MockManager. +type MockManagerMockRecorder struct { mock *MockManager } +// NewMockManager creates a new mock instance. func NewMockManager(ctrl *gomock.Controller) *MockManager { mock := &MockManager{ctrl: ctrl} - mock.recorder = &_MockManagerRecorder{mock} + mock.recorder = &MockManagerMockRecorder{mock} return mock } -func (_m *MockManager) EXPECT() *_MockManagerRecorder { - return _m.recorder +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManager) EXPECT() *MockManagerMockRecorder { + return m.recorder } -func (_m *MockManager) Create(_param0 ladon.Policy) error { - ret := _m.ctrl.Call(_m, "Create", _param0) +// Create mocks base method. +func (m *MockManager) Create(arg0 context.Context, arg1 ladon.Policy) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockManagerRecorder) Create(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "Create", arg0) +// Create indicates an expected call of Create. +func (mr *MockManagerMockRecorder) Create(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockManager)(nil).Create), arg0, arg1) } -func (_m *MockManager) Delete(_param0 string) error { - ret := _m.ctrl.Call(_m, "Delete", _param0) +// Delete mocks base method. +func (m *MockManager) Delete(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockManagerRecorder) Delete(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "Delete", arg0) +// Delete indicates an expected call of Delete. +func (mr *MockManagerMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockManager)(nil).Delete), arg0, arg1) } -func (_m *MockManager) FindRequestCandidates(_param0 *ladon.Request) (ladon.Policies, error) { - ret := _m.ctrl.Call(_m, "FindRequestCandidates", _param0) +// FindPoliciesForResource mocks base method. +func (m *MockManager) FindPoliciesForResource(arg0 context.Context, arg1 string) (ladon.Policies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindPoliciesForResource", arg0, arg1) ret0, _ := ret[0].(ladon.Policies) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) FindRequestCandidates(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "FindRequestCandidates", arg0) +// FindPoliciesForResource indicates an expected call of FindPoliciesForResource. +func (mr *MockManagerMockRecorder) FindPoliciesForResource(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPoliciesForResource", reflect.TypeOf((*MockManager)(nil).FindPoliciesForResource), arg0, arg1) } -func (_m *MockManager) FindPoliciesForSubject(_param0 string) (ladon.Policies, error) { - ret := _m.ctrl.Call(_m, "FindPoliciesForSubject", _param0) +// FindPoliciesForSubject mocks base method. +func (m *MockManager) FindPoliciesForSubject(arg0 context.Context, arg1 string) (ladon.Policies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindPoliciesForSubject", arg0, arg1) ret0, _ := ret[0].(ladon.Policies) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) FindPoliciesForSubject(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "FindPoliciesForSubject", arg0) +// FindPoliciesForSubject indicates an expected call of FindPoliciesForSubject. +func (mr *MockManagerMockRecorder) FindPoliciesForSubject(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindPoliciesForSubject", reflect.TypeOf((*MockManager)(nil).FindPoliciesForSubject), arg0, arg1) } -func (_m *MockManager) FindPoliciesForResource(_param0 string) (ladon.Policies, error) { - ret := _m.ctrl.Call(_m, "FindPoliciesForResource", _param0) +// FindRequestCandidates mocks base method. +func (m *MockManager) FindRequestCandidates(arg0 context.Context, arg1 *ladon.Request) (ladon.Policies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindRequestCandidates", arg0, arg1) ret0, _ := ret[0].(ladon.Policies) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) FindPoliciesForResource(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "FindPoliciesForResource", arg0) +// FindRequestCandidates indicates an expected call of FindRequestCandidates. +func (mr *MockManagerMockRecorder) FindRequestCandidates(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRequestCandidates", reflect.TypeOf((*MockManager)(nil).FindRequestCandidates), arg0, arg1) } -func (_m *MockManager) Get(_param0 string) (ladon.Policy, error) { - ret := _m.ctrl.Call(_m, "Get", _param0) +// Get mocks base method. +func (m *MockManager) Get(arg0 context.Context, arg1 string) (ladon.Policy, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1) ret0, _ := ret[0].(ladon.Policy) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) Get(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0) +// Get indicates an expected call of Get. +func (mr *MockManagerMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockManager)(nil).Get), arg0, arg1) } -func (_m *MockManager) GetAll(_param0 int64, _param1 int64) (ladon.Policies, error) { - ret := _m.ctrl.Call(_m, "GetAll", _param0, _param1) +// GetAll mocks base method. +func (m *MockManager) GetAll(arg0 context.Context, arg1, arg2 int64) (ladon.Policies, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", arg0, arg1, arg2) ret0, _ := ret[0].(ladon.Policies) ret1, _ := ret[1].(error) return ret0, ret1 } -func (_mr *_MockManagerRecorder) GetAll(arg0, arg1 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "GetAll", arg0, arg1) +// GetAll indicates an expected call of GetAll. +func (mr *MockManagerMockRecorder) GetAll(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockManager)(nil).GetAll), arg0, arg1, arg2) } -func (_m *MockManager) Update(_param0 ladon.Policy) error { - ret := _m.ctrl.Call(_m, "Update", _param0) +// Update mocks base method. +func (m *MockManager) Update(arg0 context.Context, arg1 ladon.Policy) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -func (_mr *_MockManagerRecorder) Update(arg0 interface{}) *gomock.Call { - return _mr.mock.ctrl.RecordCall(_mr.mock, "Update", arg0) +// Update indicates an expected call of Update. +func (mr *MockManagerMockRecorder) Update(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockManager)(nil).Update), arg0, arg1) } diff --git a/matcher_regexp.go b/matcher_regexp.go index 6357a9a..598c493 100644 --- a/matcher_regexp.go +++ b/matcher_regexp.go @@ -21,9 +21,9 @@ package ladon import ( - "regexp" "strings" + "github.com/dlclark/regexp2" "github.com/hashicorp/golang-lru" "github.com/pkg/errors" @@ -44,27 +44,25 @@ func NewRegexpMatcher(size int) *RegexpMatcher { type RegexpMatcher struct { *lru.Cache - - C map[string]*regexp.Regexp } -func (m *RegexpMatcher) get(pattern string) *regexp.Regexp { +func (m *RegexpMatcher) get(pattern string) *regexp2.Regexp { if val, ok := m.Cache.Get(pattern); !ok { return nil - } else if reg, ok := val.(*regexp.Regexp); !ok { + } else if reg, ok := val.(*regexp2.Regexp); !ok { return nil } else { return reg } } -func (m *RegexpMatcher) set(pattern string, reg *regexp.Regexp) { +func (m *RegexpMatcher) set(pattern string, reg *regexp2.Regexp) { m.Cache.Add(pattern, reg) } // Matches a needle with an array of regular expressions and returns true if a match was found. func (m *RegexpMatcher) Matches(p Policy, haystack []string, needle string) (bool, error) { - var reg *regexp.Regexp + var reg *regexp2.Regexp var err error for _, h := range haystack { @@ -80,7 +78,12 @@ func (m *RegexpMatcher) Matches(p Policy, haystack []string, needle string) (boo } if reg = m.get(h); reg != nil { - if reg.MatchString(needle) { + if matched, err := reg.MatchString(needle); err != nil { + // according to regexp2 documentation: https://github.com/dlclark/regexp2#usage + // The only error that the *Match* methods should return is a Timeout if you set the + // re.MatchTimeout field. Any other error is a bug in the regexp2 package. + return false, errors.WithStack(err) + } else if matched { return true, nil } continue @@ -92,7 +95,12 @@ func (m *RegexpMatcher) Matches(p Policy, haystack []string, needle string) (boo } m.set(h, reg) - if reg.MatchString(needle) { + if matched, err := reg.MatchString(needle); err != nil { + // according to regexp2 documentation: https://github.com/dlclark/regexp2#usage + // The only error that the *Match* methods should return is a Timeout if you set the + // re.MatchTimeout field. Any other error is a bug in the regexp2 package. + return false, errors.WithStack(err) + } else if matched { return true, nil } } diff --git a/metric.go b/metric.go new file mode 100644 index 0000000..2f607d1 --- /dev/null +++ b/metric.go @@ -0,0 +1,33 @@ +/* + * Copyright © 2016-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @copyright 2015-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package ladon + +// Metric is used to expose metrics about authz +type Metric interface { + // RequestDeniedBy is called when we get explicit deny by policy + RequestDeniedBy(Request, Policy) + // RequestAllowedBy is called when a matching policy has been found. + RequestAllowedBy(Request, Policies) + // RequestNoMatch is called when no policy has matched our request + RequestNoMatch(Request) + // RequestProcessingError is called when unexpected error occured + RequestProcessingError(Request, Policy, error) +} diff --git a/metric_noop.go b/metric_noop.go new file mode 100644 index 0000000..931d95e --- /dev/null +++ b/metric_noop.go @@ -0,0 +1,31 @@ +/* + * Copyright © 2016-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @copyright 2015-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package ladon + +// MetricNoOp is the default metrics implementation , that tracks nothing. +type MetricNoOp struct{} + +func (*MetricNoOp) RequestDeniedBy(r Request, p Policy) {} +func (*MetricNoOp) RequestAllowedBy(r Request, p Policies) {} +func (*MetricNoOp) RequestNoMatch(r Request) {} +func (*MetricNoOp) RequestProcessingError(r Request, p Policy, err error) {} + +var DefaultMetric = &MetricNoOp{} diff --git a/warden.go b/warden.go index fd46b82..2d45a52 100644 --- a/warden.go +++ b/warden.go @@ -20,6 +20,8 @@ package ladon +import "context" + // Request is the warden's request object. type Request struct { // Resource is the resource that access is requested to. @@ -41,5 +43,5 @@ type Warden interface { // if err := guard.IsAllowed(&Request{Resource: "article/1234", Action: "update", Subject: "peter"}); err != nil { // return errors.New("Not allowed") // } - IsAllowed(r *Request) error + IsAllowed(ctx context.Context, r *Request) error } diff --git a/warden_test.go b/warden_test.go index d9abb17..24a90e9 100644 --- a/warden_test.go +++ b/warden_test.go @@ -21,6 +21,7 @@ package ladon_test import ( + "context" "fmt" "testing" @@ -40,6 +41,8 @@ func TestWardenIsGranted(t *testing.T) { Manager: m, } + ctx := context.Background() + for k, c := range []struct { r *Request description string @@ -50,7 +53,7 @@ func TestWardenIsGranted(t *testing.T) { description: "should fail because no policies are found for peter", r: &Request{Subject: "peter"}, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, nil) + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, nil) }, expectErr: true, }, @@ -58,7 +61,7 @@ func TestWardenIsGranted(t *testing.T) { description: "should fail because lookup failure when accessing policies for peter", r: &Request{Subject: "peter"}, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, errors.New("asdf")) + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{Subject: "peter"})).Return(Policies{}, errors.New("asdf")) }, expectErr: true, }, @@ -70,7 +73,7 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ Subject: "peter", Resource: "articles:1234", Action: "view", @@ -93,7 +96,7 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ Subject: "ken", Resource: "articles:1234", Action: "view", @@ -116,7 +119,7 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ Subject: "ken", Resource: "printers:321", Action: "view", @@ -139,7 +142,7 @@ func TestWardenIsGranted(t *testing.T) { Action: "view", }, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ Subject: "ken", Resource: "articles:321", Action: "view", @@ -162,7 +165,7 @@ func TestWardenIsGranted(t *testing.T) { Action: "foo", }, setup: func() { - m.EXPECT().FindRequestCandidates(gomock.Eq(&Request{ + m.EXPECT().FindRequestCandidates(ctx, gomock.Eq(&Request{ Subject: "ken", Resource: "articles:321", Action: "foo", @@ -180,7 +183,7 @@ func TestWardenIsGranted(t *testing.T) { } { t.Run(fmt.Sprintf("case=%d/description=%s", k, c.description), func(t *testing.T) { c.setup() - err := w.IsAllowed(c.r) + err := w.IsAllowed(ctx, c.r) if c.expectErr { assert.NotNil(t, err) } else {