commit 9456a4e922c4a36677d8e181cc77fb8f1d4bc6fd Author: upstream-mirror Date: Wed Sep 16 01:08:34 2026 +0500 snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted) diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 0000000..8626eb4 --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,22 @@ +FROM alpine:3.21.3 + +RUN addgroup -S ory; \ + adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \ + chown -R ory:ory /home/ory + +RUN apk --no-cache --latest upgrade +RUN apk --no-cache --upgrade --latest add ca-certificates + +COPY keto /usr/bin/keto + +# Exposing the ory home directory to simplify passing in keto configuration (e.g. if the file $HOME/keto.yaml +# exists, it will be automatically used as the configuration file). +VOLUME /home/ory + +# Declare the standard ports used by keto (4433 for read service endpoint, 4434 for write service endpoint) +EXPOSE 4433 4434 + +USER ory + +ENTRYPOINT ["keto"] +CMD ["serve"] diff --git a/.docker/Dockerfile-alpine b/.docker/Dockerfile-alpine new file mode 100644 index 0000000..0dadaf1 --- /dev/null +++ b/.docker/Dockerfile-alpine @@ -0,0 +1,14 @@ +FROM alpine:3.21.3 + +RUN addgroup -S ory; \ + adduser -S ory -G ory -D -H -s /bin/nologin + +RUN apk --no-cache --latest upgrade &&\ + apk --no-cache --upgrade --latest add ca-certificates + +COPY keto /usr/bin/keto + +USER ory + +ENTRYPOINT ["keto"] +CMD ["serve"] diff --git a/.docker/Dockerfile-build b/.docker/Dockerfile-build new file mode 100644 index 0000000..79d6731 --- /dev/null +++ b/.docker/Dockerfile-build @@ -0,0 +1,38 @@ +# Workaround for https://github.com/GoogleContainerTools/distroless/issues/1342 +FROM golang:1.26-trixie AS builder + +WORKDIR /go/src/github.com/ory/keto + +RUN apt-get update && apt-get upgrade -y &&\ + mkdir -p /var/lib/sqlite &&\ + mkdir -p ./internal/httpclient + +COPY go.mod go.mod +COPY go.sum go.sum + +COPY proto/go.mod proto/go.mod +COPY proto/go.sum proto/go.sum +COPY oryx/go.mod oryx/go.mod +COPY oryx/go.sum oryx/go.sum + +ENV CGO_ENABLED=1 + +RUN go mod download + +COPY . . + +RUN go build -buildvcs=false -o /usr/bin/keto . + +######################### + +FROM gcr.io/distroless/base-nossl-debian12:nonroot AS runner + +COPY --from=builder --chown=nonroot:nonroot /var/lib/sqlite /var/lib/sqlite +COPY --from=builder /usr/bin/keto /usr/bin/keto + +VOLUME /var/lib/sqlite + +EXPOSE 4466 4467 + +ENTRYPOINT ["keto"] +CMD ["serve"] diff --git a/.docker/Dockerfile-distroless-static b/.docker/Dockerfile-distroless-static new file mode 100644 index 0000000..d61d552 --- /dev/null +++ b/.docker/Dockerfile-distroless-static @@ -0,0 +1,7 @@ +FROM gcr.io/distroless/static-debian12:nonroot + +COPY keto /usr/bin/keto +EXPOSE 4466 4467 + +ENTRYPOINT ["keto"] +CMD ["serve"] diff --git a/.docker/Dockerfile-sqlite b/.docker/Dockerfile-sqlite new file mode 100644 index 0000000..8bd373c --- /dev/null +++ b/.docker/Dockerfile-sqlite @@ -0,0 +1,37 @@ +FROM alpine:3.21.3 + +# Because this image is built for SQLite, we create /home/ory and /home/ory/sqlite which is owned by the ory user +# and declare /home/ory/sqlite a volume. +# +# To get SQLite and Docker Volumes working with this image, mount the volume where SQLite should be written to at: +# +# /home/ory/sqlite/some-file. + +RUN addgroup -S ory; \ + adduser -S ory -G ory -D -h /home/ory -s /bin/nologin; \ + chown -R ory:ory /home/ory + +RUN apk --no-cache --latest upgrade &&\ + apk --no-cache --upgrade --latest add ca-certificates + +WORKDIR /home/ory + +COPY keto /usr/bin/keto + +# By creating the sqlite folder as the ory user, the mounted volume will be owned by ory:ory, which +# is required for read/write of SQLite. +RUN mkdir -p /var/lib/sqlite +RUN chown ory:ory /var/lib/sqlite +VOLUME /var/lib/sqlite + +# Exposing the ory home directory to simplify passing in keto configuration (e.g. if the file $HOME/.keto.yaml +# exists, it will be automatically used as the configuration file). +VOLUME /home/ory + +# Declare the standard ports used by keto (4433 for read service endpoint, 4434 for write service endpoint) +EXPOSE 4433 4434 + +USER ory + +ENTRYPOINT ["keto"] +CMD ["serve"] diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8b91d5a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +docs/ +scripts/ +contrib/ +.bin/ +.github/ +*.yaml +*.yml +*.json +*.md +*ignore +Makefile diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a05728 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +internal/httpclient/**/* linguist-generated=true +proto/**/*.pb.go linguist-generated=true +proto/**/*_pb.js linguist-generated=true +proto/**/*_pb.d.ts linguist-generated=true +.bin/formula-pins/*.rb linguist-generated=true +.fuzzer/** linguist-generated=true diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..ef90d00 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @aeneasr @ory/product-development 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..785d490 --- /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/keto/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/keto/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..17e96f5 --- /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/keto/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/keto/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..56bf9bc --- /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/keto/blob/master/CODE_OF_CONDUCT.md)." + required: true + - label: + "I have read and am following this repository's [Contribution + Guidelines](https://github.com/ory/keto/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..3d65dea --- /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 Keto Forum + url: https://github.com/ory/keto/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/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000..c6cf23b --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,16 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/auto_assign.yml + +# Set to true to add reviewers to pull requests +addReviewers: true + +# Set to true to add assignees to pull requests +addAssignees: true + +# A list of reviewers to be added to pull requests (GitHub user name) +assignees: + - ory/maintainers + +# A number of reviewers added to the pull request +# Set 0 to add all the reviewers (default: 0) +numberOfReviewers: 0 diff --git a/.github/config.yml b/.github/config.yml new file mode 100644 index 0000000..4fed118 --- /dev/null +++ b/.github/config.yml @@ -0,0 +1,6 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/.github/config.yml + +todo: + keyword: "@todo" + label: todo diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5992c55 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: "gomod" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "gomod" + directory: "/gen/go" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "npm" + directory: "/gen/es" + schedule: + interval: "daily" + - package-ecosystem: "docker" + directory: "/.docker" + schedule: + interval: "daily" diff --git a/.github/labels.json b/.github/labels.json new file mode 100644 index 0000000..7545335 --- /dev/null +++ b/.github/labels.json @@ -0,0 +1,12 @@ +[ + { + "name": "dependencies", + "color": "0366d6", + "description": "Pull requests that update a dependency file" + }, + { + "name": "docker", + "color": "21ceff", + "description": "Pull requests that update Docker code" + } +] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..32c141f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,60 @@ + + +## Related issue(s) + + + +## Checklist + + + +- [ ] I have read the [contributing guidelines](../blob/master/CONTRIBUTING.md). +- [ ] I have referenced an issue containing the design document if my change + introduces a new feature. +- [ ] I am following the + [contributing code guidelines](../blob/master/CONTRIBUTING.md#contributing-code). +- [ ] 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 the 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 or changed [the documentation](https://github.com/ory/docs). + +## Further Comments + + diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..d1fcd01 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,242 @@ +name: CI +on: + push: + branches: + - master + tags: + - "*" + pull_request: + merge_group: + +# Cancel in-progress runs in current workflow. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: Run lints and checks + runs-on: ubuntu-latest + steps: + - uses: ory/ci/checkout@master + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - name: Update npm to a non-buggy version + run: sudo npm i -g npm@9.5.0 + - name: Lint buf + run: make buf-lint + - run: go list -json > go.list + - name: Run nancy + uses: sonatype-nexus-community/nancy-github-action@395e2fb168f674f96502e5652103d112899ea369 + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Run golangci-lint + if: ${{ github.ref_type != 'tag' }} + uses: golangci/golangci-lint-action@v9 + env: + GOGC: 100 + with: + version: latest + args: --timeout 10m0s + only-new-issues: "true" + - name: Test documentation examples + run: make test-docs-samples + - name: Test OPL typelib + run: cd contrib/namespace-type-lib && npm ci && npm test + + test: + name: Run tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres:18 + env: + POSTGRES_DB: keto + POSTGRES_PASSWORD: test + POSTGRES_USER: test + ports: + - 5432:5432 + mysql: + image: mysql:9.6 + env: + MYSQL_ROOT_PASSWORD: test + ports: + - 3306:3306 + env: + TEST_DATABASE_POSTGRESQL: postgres://test:test@localhost:5432/keto?sslmode=disable + TEST_DATABASE_MYSQL: mysql://root:test@(localhost:3306)/mysql?parseTime=true&multiStatements=true + TEST_DATABASE_COCKROACHDB: cockroach://root@localhost:26257/defaultdb?sslmode=disable + steps: + - run: | + docker create --name cockroach -p 26257:26257 \ + cockroachdb/cockroach:latest-v25.4 start-single-node --insecure + docker start cockroach + name: Start CockroachDB + - uses: ory/ci/checkout@master + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - name: Test gen/go package + run: go -C gen/go test ./... + - name: Test Keto + run: | + go test -coverprofile coverage.txt ./... + grep -v "ory/keto/internal/httpclient" coverage.txt > coverage_cleared.txt + mv coverage_cleared.txt coverage.txt + - run: | + go tool goveralls -service=github -coverprofile=coverage.txt + env: + COVERALLS_TOKEN: ${{ github.token }} + + test-race: + name: Run race tests + runs-on: ubuntu-latest + env: + CGO_ENABLED: "1" + steps: + - uses: ory/ci/checkout@master + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - run: | + go test -race -short ./... + + docs-cli: + runs-on: ubuntu-latest + name: Build CLI docs + needs: + - test + - test-race + - validate + steps: + - uses: ory/ci/docs/cli-next@master + with: + token: ${{ secrets.ORY_BOT_PAT }} + output-dir: docs/keto/cli + + release: + name: Generate release + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: + - test + - test-race + - validate + steps: + - uses: ory/ci/releaser@master + with: + token: ${{ secrets.ORY_BOT_PAT }} + goreleaser_key: ${{ secrets.GORELEASER_KEY }} + cosign_pwd: ${{ secrets.COSIGN_PWD }} + docker_username: ${{ secrets.DOCKERHUB_USERNAME }} + docker_password: ${{ secrets.DOCKERHUB_PASSWORD }} + + newsletter-draft: + name: Draft newsletter + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: + - release + steps: + - uses: ory/ci/newsletter@master + with: + mailchimp_list_id: f605a41b53 + mailchmip_segment_id: 6479489 + mailchimp_api_key: ${{ secrets.MAILCHIMP_API_KEY }} + draft: "true" + ssh_key: ${{ secrets.ORY_BOT_SSH_KEY }} + + slack-approval-notification: + name: Pending approval Slack notification + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + needs: + - newsletter-draft + steps: + - uses: ory/ci/newsletter/slack-notify@master + with: + slack-webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} + + newsletter-send: + name: Send newsletter + runs-on: ubuntu-latest + needs: + - newsletter-draft + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + environment: production + steps: + - uses: ory/ci/newsletter@master + with: + mailchimp_list_id: f605a41b53 + mailchmip_segment_id: 6479489 + mailchimp_api_key: ${{ secrets.MAILCHIMP_API_KEY }} + draft: "false" + ssh_key: ${{ secrets.ORY_BOT_SSH_KEY }} + + buf: + name: Run buf actions + runs-on: ubuntu-latest + steps: + - uses: ory/ci/checkout@master + with: + path: current-repo + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: current-repo/go.mod + - uses: actions/setup-node@v6 + with: + node-version: "24" + - name: Lint and Build + run: | + make buf + working-directory: current-repo + - name: Format Docs (required because buf generates the proto docs) + run: | + make format + working-directory: current-repo + - name: Push build + run: | + git config --local user.email "60093411+ory-bot@users.noreply.github.com" + git config --local user.name "ory-bot" + git add gen + git commit -a -m "autogen: build buf" && git push "https://${GITHUB_ACTOR}:${{ secrets.ORY_BOT_PAT }}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF#"refs/heads/"}" || true + working-directory: current-repo + - if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }} + uses: ory/ci/checkout@master + with: + repository: ory/docs + path: docs + fetch-depth: 0 + token: ${{ secrets.ORY_BOT_PAT }} + - if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }} + name: Push generated files to ory/docs + env: + GITHUB_TOKEN: ${{ secrets.ORY_BOT_PAT }} + run: | + echo "--- + title: Protocol buffers API + --- + + " > "docs/docs/keto/reference/proto-api.mdx" + cat "current-repo/gen/doc/buf.md" >> "docs/docs/keto/reference/proto-api.mdx" + cd docs + if git diff --exit-code; then + echo "No changes to commit" + exit 0 + fi + + git config --local user.email "60093411+ory-bot@users.noreply.github.com" + git config --local user.name "ory-bot" + + git stash + git pull --rebase origin master + git stash apply + git add docs/keto/reference/proto-api.mdx + git commit -m "autogen: update Keto protobuf docs" + git push origin HEAD:master + shell: bash diff --git a/.github/workflows/closed_references.yml b/.github/workflows/closed_references.yml new file mode 100644 index 0000000..de5efcf --- /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@v7 + - uses: actions/setup-node@v6 + with: + node-version: "24" + - 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/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..1a017f8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [master, legacy-0.5] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + merge_group: + schedule: + - cron: "39 6 * * 0" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["go", "javascript"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v4 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 diff --git a/.github/workflows/conventional_commits.yml b/.github/workflows/conventional_commits.yml new file mode 100644 index 0000000..2032849 --- /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@v7 + - 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@v6 + 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/cve-scan.yaml b/.github/workflows/cve-scan.yaml new file mode 100644 index 0000000..dbd337b --- /dev/null +++ b/.github/workflows/cve-scan.yaml @@ -0,0 +1,114 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/server/.github/workflows/cve-scan.yaml + +name: Docker Image Scanners +on: + workflow_dispatch: + push: + branches: + - "master" + tags: + - "v*.*.*" + pull_request: + branches: + - "master" + +permissions: + contents: read + security-events: write + +jobs: + scanners: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Setup Env + id: vars + shell: bash + run: | + # Store values in local variables + SHA_SHORT=$(git rev-parse --short HEAD) + REPO_NAME=${{ github.event.repository.name }} + + IMAGE_NAME="oryd/${REPO_NAME}:${SHA_SHORT}" + + # Output values for debugging + echo "Values to be set:" + echo "SHA_SHORT: ${SHA_SHORT}" + echo "REPO_NAME: ${REPO_NAME}" + echo "IMAGE_NAME: ${IMAGE_NAME}" + + # Set GitHub Environment variables + echo "SHA_SHORT=${SHA_SHORT}" >> "${GITHUB_ENV}" + echo "IMAGE_NAME=${IMAGE_NAME}" >> "${GITHUB_ENV}" + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + with: + driver: docker + - name: Build images + shell: bash + run: | + IMAGE_TAG="${{ env.SHA_SHORT }}" make docker + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Anchore Scanner + uses: anchore/scan-action@v7 + id: grype-scan + with: + image: ${{ env.IMAGE_NAME }} + fail-build: true + severity-cutoff: high + add-cpes-if-none: true + - name: Inspect action SARIF report + shell: bash + if: ${{ always() }} + run: | + echo "::group::Anchore Scan Details" + jq '.runs[0].results' ${{ steps.grype-scan.outputs.sarif }} + echo "::endgroup::" + - name: Anchore upload scan SARIF report + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: ${{ steps.grype-scan.outputs.sarif }} + - name: Kubescape scanner + uses: kubescape/github-action@main + id: kubescape + with: + image: ${{ env.IMAGE_NAME }} + verbose: true + format: pretty-printer + # can't whitelist CVE yet: https://github.com/kubescape/kubescape/pull/1568 + severityThreshold: critical + - name: Dockle Linter + uses: erzz/dockle-action@v1 + if: ${{ always() }} + with: + image: ${{ env.IMAGE_NAME }} + exit-code: 42 + failure-threshold: high + - name: Hadolint + uses: hadolint/hadolint-action@v3.3.0 + id: hadolint + if: ${{ always() }} + with: + dockerfile: .docker/Dockerfile-build + verbose: true + format: "json" + failure-threshold: "error" + - name: View Hadolint results + if: ${{ always() }} + shell: bash + run: | + echo "::group::Hadolint Scan Details" + echo "${HADOLINT_RESULTS}" | jq '.' + echo "::endgroup::" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..4b58087 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +name: Format + +on: + pull_request: + push: + merge_group: + +env: + CGO_ENABLED: "0" + +jobs: + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - run: make format + - name: Indicate formatting issues + run: git diff HEAD --exit-code --color diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml new file mode 100644 index 0000000..ed551b6 --- /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@v7 + - 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/milestone.yml b/.github/workflows/milestone.yml new file mode 100644 index 0000000..e8431e4 --- /dev/null +++ b/.github/workflows/milestone.yml @@ -0,0 +1,33 @@ +# AUTO-GENERATED, DO NOT EDIT! +# Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/server/.github/workflows/milestone.yml + +name: Generate and Publish Milestone Document + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * *" + +jobs: + milestone: + if: github.repository_owner == 'ory' + name: Generate and Publish Milestone Document + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + token: ${{ secrets.TOKEN_PRIVILEGED }} + - name: Milestone Documentation Generator + uses: ory/milestone-action@v0 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputFile: docs/docs/milestones.md + - name: Commit Milestone Documentation + uses: EndBug/add-and-commit@v10.0.0 + with: + message: "autogen(docs): update milestone document" + author_name: aeneasr + author_email: "3372410+aeneasr@users.noreply.github.com" + env: + GITHUB_TOKEN: ${{ secrets.TOKEN_PRIVILEGED }} diff --git a/.github/workflows/npm_publish_grpc_client.yml b/.github/workflows/npm_publish_grpc_client.yml new file mode 100644 index 0000000..e49974a --- /dev/null +++ b/.github/workflows/npm_publish_grpc_client.yml @@ -0,0 +1,40 @@ +name: Publish gRPC client to npm + +on: + release: + types: + - created + workflow_dispatch: + inputs: + version: + required: true + description: The version to release + +jobs: + btp: + runs-on: ubuntu-latest + name: Publish + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + - run: sudo npm i -g npm@7 + - name: Bump version + run: |- + cd gen/es + jq '.version = (env.RELEASE_VERSION | sub("(^refs/tags/v)|(^v)"; ""))' package.json > package.json.tmp \ + && mv package.json.tmp package.json + env: + RELEASE_VERSION: ${{ github.event.inputs.version || github.ref }} + - run: |- + git config --global user.email "60093411+ory-bot@users.noreply.github.com" + git config --global user.name "ory-bot" + git add gen/es/package.json + git commit -m "autogen: bump node gRPC client version" + - run: |- + cd gen/es + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_AENEASR }} diff --git a/.github/workflows/npm_publish_typelib.yml b/.github/workflows/npm_publish_typelib.yml new file mode 100644 index 0000000..e31869a --- /dev/null +++ b/.github/workflows/npm_publish_typelib.yml @@ -0,0 +1,40 @@ +name: Publish OPL typelib to npm + +on: + release: + types: + - created + workflow_dispatch: + inputs: + version: + required: true + description: The version to release + +jobs: + btp: + runs-on: ubuntu-latest + name: Publish + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + - run: sudo npm i -g npm@10 + - name: Bump version + run: |- + cd contrib/namespace-type-lib + jq '.version = (env.RELEASE_VERSION | sub("(^refs/tags/v)|(^v)"; ""))' package.json > package.json.tmp \ + && mv package.json.tmp package.json + env: + RELEASE_VERSION: ${{ github.event.inputs.version || github.ref }} + - run: |- + git config --global user.email "60093411+ory-bot@users.noreply.github.com" + git config --global user.name "ory-bot" + git add contrib/namespace-type-lib/package.json + git commit -m "autogen: bump OPL typelib" + - run: |- + cd contrib/namespace-type-lib + npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_AENEASR }} diff --git a/.github/workflows/pm.yml b/.github/workflows/pm.yml new file mode 100644 index 0000000..cec5a91 --- /dev/null +++ b/.github/workflows/pm.yml @@ -0,0 +1,30 @@ +name: Synchronize with product board + +on: + issues: + types: + - opened + pull_request: + types: + - opened + - ready_for_review + +jobs: + automate: + if: github.event.pull_request.head.repo.fork == false + name: Add issue to project + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: ory-corp/planning-automation-action@v0.2 + with: + organization: ory-corp + project: 5 + token: ${{ secrets.ORY_BOT_PAT }} + todoLabel: "Needs Triage" + statusName: Status + prStatusValue: "Needs Triage" + issueStatusValue: "Needs Triage" + includeEffort: "false" + monthlyMilestoneName: Roadmap Monthly + quarterlyMilestoneName: Roadmap diff --git a/.github/workflows/release-go-grpc-client.yml b/.github/workflows/release-go-grpc-client.yml new file mode 100644 index 0000000..360f529 --- /dev/null +++ b/.github/workflows/release-go-grpc-client.yml @@ -0,0 +1,41 @@ +name: Release the go gRPC client as `gen/go/$VERSION` + +on: + workflow_dispatch: + inputs: + version: + description: + The version to release. Should be a patch of the latest Keto release. + required: true + release: + types: + - created + +env: + CGO_ENABLED: "0" + +jobs: + tag: + # this is set on dispatch this should prevent the action from triggering itself + if: github.event.inputs.version || !startsWith(github.ref, 'gen/go/') + runs-on: ubuntu-latest + name: Publish + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - name: Download dependencies + run: go -C gen/go mod tidy + - name: Test + run: go -C gen/go test ./... + - name: Create git tag + run: | + git config --global user.email "60093411+ory-bot@users.noreply.github.com" + git config --global user.name "ory-bot" + RELEASE_VERSION="$(sed 's#^refs/tags/##' <(echo "$RELEASE_REF"))" + git tag -a "gen/go/$RELEASE_VERSION" -m "Release $RELEASE_VERSION of the go gRPC Client. See CHANGELOG.md for more info." + git push origin "gen/go/$RELEASE_VERSION" + env: + RELEASE_REF: ${{ github.event.inputs.version || github.ref }} diff --git a/.github/workflows/single-table-migration-e2e.yml b/.github/workflows/single-table-migration-e2e.yml new file mode 100644 index 0000000..19e668b --- /dev/null +++ b/.github/workflows/single-table-migration-e2e.yml @@ -0,0 +1,30 @@ +name: + Run full e2e test of the migration to single table persister (see + https://github.com/ory/keto/issues/628) + +on: + workflow_dispatch: + push: + branches: + - feat/persistence-migration-path + +env: + CGO_ENABLED: "0" + +jobs: + test-migration: + runs-on: ubuntu-latest + name: Test Migration + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-go@v6 + with: + check-latest: true + go-version-file: go.mod + - name: Run test script + run: ./scripts/single-table-migration-e2e.sh + - uses: actions/upload-artifact@v7 + if: failure() + with: + name: sqlite-db + path: migrate_e2e.sqlite diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..c1f52b1 --- /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@v10 + 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/.gitignore b/.gitignore new file mode 100644 index 0000000..ce5a5ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.bin/ +node_modules/ +.idea/ +coverage.txt +dist/ +**/*.sqlite +**/*.sqlite-journal +.vscode/ +.fuzzer/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..7f5807e --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,9 @@ +version: "2" + +linters: + enable: + - gosec + - errcheck + - ineffassign + - staticcheck + - unused diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..75c28ee --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,16 @@ +version: 2 + +project_name: keto + +includes: + - from_url: + url: https://raw.githubusercontent.com/ory/xgoreleaser/master/build.tmpl.yml + +variables: + brew_name: keto + brew_description: "The Ory Authorization Server (Ory Keto)" + buildinfo_hash: "github.com/ory/keto/internal/driver/config.Commit" + buildinfo_tag: "github.com/ory/keto/internal/driver/config.Version" + buildinfo_date: "github.com/ory/keto/internal/driver/config.Date" + dockerfile_alpine: ".docker/Dockerfile-alpine" + dockerfile_static: ".docker/Dockerfile-distroless-static" diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000..cdefcaa --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,10 @@ +only-fixed: true +ignore: + # see https://github.com/anchore/grype/issues/558 + - vulnerability: CVE-2015-5237 + - vulnerability: CVE-2021-22570 + # https://github.com/opencontainers/runc/issues/4233 + - vulnerability: CVE-2024-3154 + - package: + name: github.com/opencontainers/runc + version: v1.1.12 diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..f88de98 --- /dev/null +++ b/.mailmap @@ -0,0 +1,8 @@ +Aeneas Rekkas +Aeneas Rekkas <3372410+aeneasr@users.noreply.github.com> +Aeneas Rekkas +Aeneas Rekkas +Aeneas Rekkas +Henning Perl +Patrik Neu +Patrik Neu diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5aceeea --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +*.md +*.go +*.php +*.md +*.png +*.jpg +*.jpeg +sdk/go/* +sdk/php/* +docs/* +scripts/* +vendor/* \ No newline at end of file diff --git a/.orycli.yml b/.orycli.yml new file mode 100644 index 0000000..93a9e82 --- /dev/null +++ b/.orycli.yml @@ -0,0 +1,7 @@ +project: keto + +pre_release_hooks: + - ./scripts/render-schemas.sh + +ignore_tags: + - proto/.* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..f08d6b9 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +.bin +.snapshots +internal +cmd +ketoctx +.schema/openapi/patches +proto/**/*.js +proto/**/*.ts +oryx \ No newline at end of file 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..02fed54 --- /dev/null +++ b/.reports/dep-licenses.csv @@ -0,0 +1,6 @@ +"module name","licenses" + +"github.com/ory/x","Apache-2.0" +"github.com/stretchr/testify","MIT" +"go.opentelemetry.io/otel/sdk","Apache-2.0" + diff --git a/.schema/README.md b/.schema/README.md new file mode 100644 index 0000000..57d689b --- /dev/null +++ b/.schema/README.md @@ -0,0 +1,5 @@ +The schemas in this directory are meant for external and public use. + +The config schema is generated from the internal one at +`embedx/config.schema.json`, so in case of changes to the config schema, please +edit that internal schema instead. diff --git a/.schema/config.schema.json b/.schema/config.schema.json new file mode 100644 index 0000000..e6b1c4d --- /dev/null +++ b/.schema/config.schema.json @@ -0,0 +1,508 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/keto/master/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Keto Configuration", + "type": "object", + "definitions": { + "$schema": { + "type": "string", + "format": "uri-reference", + "description": "Add this to allow defining the schema, useful for IDE integration" + }, + "namespace": { + "type": "object", + "properties": { + "$schema": { + "$ref": "#/definitions/$schema" + }, + "name": { + "type": "string", + "title": "The name of the namespace.", + "examples": ["videos", "groups", "files"] + }, + "id": { + "type": "integer", + "title": "The unique ID of the namespace. Can not be changed once set.", + "minimum": 0, + "maximum": 2147483647 + }, + "config": { + "type": "object", + "title": "The configuration of the namespace.", + "description": "To be defined." + } + }, + "additionalProperties": false, + "required": ["name"] + }, + "tlsxSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + }, + "tlsx": { + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + } + } + }, + "cors": { + "title": "Cross Origin Resource Sharing (CORS)", + "description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "title": "Enable CORS", + "description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered." + }, + "allowed_origins": { + "title": "Allowed Origins", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "default": ["*"], + "uniqueItems": true, + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "title": "Allowed HTTP Methods", + "description": "A list of methods the client is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ] + }, + "uniqueItems": true, + "default": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "allowed_headers": { + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "title": "Allowed Request HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Authorization", "Content-Type"] + }, + "exposed_headers": { + "description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "title": "Allowed Response HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Content-Type"] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow HTTP Credentials", + "default": false, + "description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates." + }, + "max_age": { + "type": "integer", + "default": 0, + "title": "Maximum Age", + "description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age." + }, + "debug": { + "type": "boolean", + "default": false, + "title": "Enable Debugging", + "description": "Set to true to debug server side CORS issues." + } + }, + "additionalProperties": false + } + }, + "required": ["dsn"], + "additionalProperties": false, + "properties": { + "$schema": { + "$ref": "#/definitions/$schema" + }, + "dsn": { + "title": "Data Source Name", + "type": "string", + "description": "Sets the data source name. This configures the backend where ORY Keto persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.com/docs/hydra/dependencies-environment#sql", + "examples": [ + "postgres://user:password@host:123/database", + "mysql://user:password@tcp(host:123)/database", + "memory" + ] + }, + "serve": { + "type": "object", + "additionalProperties": false, + "properties": { + "read": { + "type": "object", + "title": "Read API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4466, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Read Listen File", + "description": "The path to a file that will be created when the read API is ready to accept connections. The content of the file is the host:port of the read API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-read-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "write": { + "type": "object", + "title": "Write API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4467, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Write Listen File", + "description": "The path to a file that will be created when the write API is ready to accept connections. The content of the file is the host:port of the write API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-write-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "metrics": { + "type": "object", + "title": "Metrics API (http only)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4468, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Metrics Listen File", + "description": "The path to a file that will be created when the metrics API is ready to accept connections. The content of the file is the host:port of the metrics API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-metrics-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "opl": { + "type": "object", + "title": "Ory Permission Language Syntax API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4469, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "OPL Listen File", + "description": "The path to a file that will be created when the OPL API is ready to accept connections. The content of the file is the host:port of the OPL API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-opl-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + } + } + }, + "profiling": { + "title": "Profiling", + "description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "type": "string", + "enum": ["cpu", "mem", ""] + }, + "log": { + "$ref": "https://raw.githubusercontent.com/ory/keto/6e32173d/oryx/logrusx/config.schema.json" + }, + "tracing": { + "$ref": "https://raw.githubusercontent.com/ory/keto/6e32173d/oryx/otelx/config.schema.json" + }, + "namespaces": { + "description": "Namespace configuration or it's location.", + "default": "file://./keto_namespaces", + "oneOf": [ + { + "title": "Legacy namespace Repo URI", + "description": "A URI that points to a directory of namespace files, or a single file with all namespaces", + "type": "string", + "format": "uri" + }, + { + "type": "array", + "title": "Legacy namespace configuration", + "items": { + "$ref": "#/definitions/namespace" + } + }, + { + "type": "object", + "title": "Namespace configuration", + "properties": { + "location": { + "type": "string", + "title": "Ory Permission Language config file URI", + "description": "A URI that points to a directory of namespace files, or a single file with all namespaces", + "format": "uri", + "examples": [ + "file://./keto_namespaces.ts", + "file:///etc/configs/keto_namespaces.ts" + ] + }, + "experimental_strict_mode": { + "type": "boolean", + "title": "Strict permission checking mode (deprecated)", + "description": "Deprecated: use feature_flags.strict_mode instead. If strict mode is enabled, then relation tuples for permits are not checked directly (but the rewrites are applied). Similarly, subject sets are only expanded if they were declared with SubjectSet<...>. These stricter rules result in much faster checks with fewer queries to the underlying database." + } + }, + "additionalProperties": false, + "required": ["location"] + } + ] + }, + "limit": { + "type": "object", + "title": "Limits", + "description": "Limits aiming to control the resource consumption. These limits are not a sufficient replacement for rate-limiting.", + "properties": { + "max_read_depth": { + "type": "integer", + "default": 5, + "title": "Global maximum read depth", + "description": "The global maximum depth on all read operations. Note that this does not affect how deeply nested the tuples can be. This value can be decreased for a request by a value specified on the request, only if the request-specific value is greater than 1 and less than the global maximum depth.", + "minimum": 1, + "maximum": 65535 + }, + "max_read_width": { + "type": "integer", + "default": 100, + "title": "Global maximum read width", + "description": "The global maximum width on all read operations. Note that this does not affect how deeply nested the tuples can be. This value can be decreased for a request by a value specified on the request, only if the request-specific value is greater than 1 and less than the global maximum width.", + "minimum": 1, + "maximum": 65535 + }, + "max_batch_check_size": { + "type": "integer", + "default": 10, + "title": "Maximum batch check size", + "description": "The maximum number of tuples that will be accepted by the batch check endpoint.", + "minimum": 1, + "maximum": 65535 + }, + "batch_check_max_parallelization": { + "type": "integer", + "default": 5, + "title": "Max concurrent checks during batch check", + "description": "The limit for the number of tuples that will be checked concurrently during a batch check.", + "minimum": 1, + "maximum": 65535 + }, + "max_expand_size": { + "type": "integer", + "default": 4000, + "title": "Maximum tuples during expand", + "description": "The maximum total number of relation tuples the expand engine will return in a single BuildTree call. Once the limit is reached, further nodes are returned as truncated.", + "minimum": 0, + "maximum": 65535 + } + }, + "additionalProperties": false + }, + "clients": { + "title": "Global outgoing network settings", + "description": "Configure how outgoing network calls behave.", + "type": "object", + "properties": { + "http": { + "title": "Global HTTP client configuration", + "description": "Configure how outgoing HTTP calls behave.", + "type": "object", + "properties": { + "disallow_private_ip_ranges": { + "title": "Disallow private IP ranges", + "description": "Disallow all outgoing HTTP calls to private IP ranges. This feature can help protect against SSRF attacks.", + "type": "boolean", + "default": false + } + } + } + } + }, + "secrets": { + "title": "Secrets for signing and encryption", + "description": "Make sure to use unique and random secrets for production deployments. Horizontally scaled deployments have to use the same secrets across all instances.", + "type": "object", + "properties": { + "pagination": { + "description": "Pagination secrets are used to encrypt pagination tokens. The first secret is used for encryption, while all others are used for decryption. This allows you to rotate the encryption key without invalidating all existing pagination tokens.", + "type": "array", + "minLength": 1, + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "secret used for encryption", + "old secret kept for decryption", + "another old secret kept for decryption" + ] + ] + } + } + }, + "feature_flags": { + "title": "Feature flags", + "description": "Opt in to features that are still being refined. Behavior may change in future releases.", + "type": "object", + "additionalProperties": false, + "properties": { + "expand_rewrites": { + "type": "boolean", + "title": "Expand OPL rewrite rules", + "description": "If enabled, the expand API follows OPL rewrite rules when building permission trees.", + "default": false + }, + "strict_mode": { + "type": "boolean", + "title": "Strict permission checking mode", + "description": "If strict mode is enabled, then relation tuples for permits are not checked directly (but the rewrites are applied). Similarly, subject sets are only expanded if they were declared with SubjectSet<...>. These stricter rules result in much faster checks with fewer queries to the underlying database.", + "default": false + } + } + }, + "version": { + "type": "string", + "title": "The Keto version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } +} diff --git a/.schema/namespace.schema.json b/.schema/namespace.schema.json new file mode 100644 index 0000000..854121e --- /dev/null +++ b/.schema/namespace.schema.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "./config.schema.json#/definitions/namespace", + "description": "This schema is just to work around the limitation of some IDEs to support json schema paths." +} diff --git a/.schema/openapi/gen.go.yml b/.schema/openapi/gen.go.yml new file mode 100644 index 0000000..48cee4d --- /dev/null +++ b/.schema/openapi/gen.go.yml @@ -0,0 +1,6 @@ +disallowAdditionalPropertiesIfNotPresent: true +packageName: client +generateInterfaces: true +isGoSubmodule: false +structPrefix: true +enumClassPrefix: true diff --git a/.schema/openapi/gen.typescript.yml b/.schema/openapi/gen.typescript.yml new file mode 100644 index 0000000..8030dff --- /dev/null +++ b/.schema/openapi/gen.typescript.yml @@ -0,0 +1,7 @@ +npmName: "@ory/kratos-client" +npmVersion: 0.0.0 +# typescriptThreePlus: true +#npmRepository: https://github.com/ory/sdk.git +supportsES6: true +ensureUniqueParams: true +modelPropertyNaming: original diff --git a/.schema/openapi/patches/health.yaml b/.schema/openapi/patches/health.yaml new file mode 100644 index 0000000..1d42a1d --- /dev/null +++ b/.schema/openapi/patches/health.yaml @@ -0,0 +1,94 @@ +- op: replace + path: /paths/~1health~1alive + value: + get: + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the health status will never + refer to the cluster state, only to a single instance. + operationId: isAlive + responses: + '200': + content: + application/json: + schema: + "$ref": "#/components/schemas/healthStatus" + description: {{.ProjectHumanName}} is ready to accept connections. + '500': + content: + application/json: + schema: + "$ref": "#/components/schemas/genericError" + description: genericError + summary: Check HTTP Server Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1health~1ready + value: + get: + operationId: isReady + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of {{.ProjectHumanName}}, the health status will never + refer to the cluster state, only to a single instance. + responses: + '200': + content: + application/json: + schema: + type: object + properties: + status: + description: Always "ok". + type: string + description: {{.ProjectHumanName}} is ready to accept requests. + '503': + content: + application/json: + schema: + properties: + errors: + additionalProperties: + type: string + description: Errors contains a list of errors that caused the not ready status. + type: object + type: object + description: Ory Kratos is not yet ready to accept requests. + summary: Check HTTP Server and Database Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1version + value: + get: + description: |- + This endpoint returns the version of {{.ProjectHumanName}}. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the version will never + refer to the cluster state, only to a single instance. + operationId: getVersion + responses: + '200': + content: + application/json: + schema: + type: object + properties: + version: + description: The version of {{.ProjectHumanName}}. + type: string + description: Returns the {{.ProjectHumanName}} version. + summary: Return Running Software Version. + tags: {{ .HealthPathTags | toJson }} diff --git a/.schema/openapi/patches/meta.yaml b/.schema/openapi/patches/meta.yaml new file mode 100644 index 0000000..8df46a3 --- /dev/null +++ b/.schema/openapi/patches/meta.yaml @@ -0,0 +1,12 @@ +- op: replace + path: /info + value: + title: Ory Keto API + description: | + Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + version: >- + {{ getenv "CIRCLE_TAG" }} + license: + name: Apache 2.0 + contact: + email: "hi@ory.sh" diff --git a/.schema/openapi/templates/go/.travis.yml b/.schema/openapi/templates/go/.travis.yml new file mode 100644 index 0000000..755978d --- /dev/null +++ b/.schema/openapi/templates/go/.travis.yml @@ -0,0 +1,7 @@ +language: go + +install: + - go get -d -v . + +script: + - go build -v ./ diff --git a/.schema/openapi/templates/go/README.mustache b/.schema/openapi/templates/go/README.mustache new file mode 100644 index 0000000..b75fa1b --- /dev/null +++ b/.schema/openapi/templates/go/README.mustache @@ -0,0 +1,221 @@ +# Go API client for {{packageName}} + +{{#appDescriptionWithNewLines}} +{{{appDescriptionWithNewLines}}} +{{/appDescriptionWithNewLines}} + +## Overview +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. + +- API version: {{appVersion}} +- Package version: {{packageVersion}} +{{^hideGenerationTimestamp}} +- Build date: {{generatedDate}} +{{/hideGenerationTimestamp}} +- Build package: {{generatorClass}} +{{#infoUrl}} +For more information, please visit [{{{infoUrl}}}]({{{infoUrl}}}) +{{/infoUrl}} + +## Installation + +Install the following dependencies: + +```shell +go get github.com/stretchr/testify/assert +go get golang.org/x/oauth2 +go get golang.org/x/net/context +``` + +Put the package under your project folder and add the following in import: + +```golang +import {{packageName}} "{{gitHost}}/{{gitUserId}}/{{gitRepoId}}{{#isGoSubmodule}}/{{packageName}}{{/isGoSubmodule}}" +``` + +To use a proxy, set the environment variable `HTTP_PROXY`: + +```golang +os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") +``` + +## Configuration of Server URL + +Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. + +### Select Server Configuration + +For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`. + +```golang +ctx := context.WithValue(context.Background(), {{packageName}}.ContextServerIndex, 1) +``` + +### Templated Server URL + +Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`. + +```golang +ctx := context.WithValue(context.Background(), {{packageName}}.ContextServerVariables, map[string]string{ + "basePath": "v2", +}) +``` + +Note, enum values are always validated and all unused variables are silently ignored. + +### URLs Configuration per Operation + +Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. +An operation is uniquely identifield by `"{classname}Service.{nickname}"` string. +Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps. + +``` +ctx := context.WithValue(context.Background(), {{packageName}}.ContextOperationServerIndices, map[string]int{ + "{classname}Service.{nickname}": 2, +}) +ctx = context.WithValue(context.Background(), {{packageName}}.ContextOperationServerVariables, map[string]map[string]string{ + "{classname}Service.{nickname}": { + "port": "8443", + }, +}) +``` + +## Documentation for API Endpoints + +All URIs are relative to *{{basePath}}* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}} +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + +## Documentation For Models + +{{#models}}{{#model}} - [{{{classname}}}]({{modelDocPath}}{{{classname}}}.md) +{{/model}}{{/models}} + +## Documentation For Authorization + +{{^authMethods}} Endpoints do not require authorization. +{{/authMethods}}{{#authMethods}}{{#last}} Authentication schemes defined for the API:{{/last}}{{/authMethods}} +{{#authMethods}} + +### {{{name}}} + +{{#isApiKey}} +- **Type**: API key +- **API key parameter name**: {{{keyParamName}}} +- **Location**: {{#isKeyInQuery}}URL query string{{/isKeyInQuery}}{{#isKeyInHeader}}HTTP header{{/isKeyInHeader}} + +Note, each API key must be added to a map of `map[string]APIKey` where the key is: {{keyParamName}} and passed in as the auth context for each request. + +{{/isApiKey}} +{{#isBasic}} +{{#isBasicBearer}} +- **Type**: HTTP Bearer token authentication + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextAccessToken, "BEARER_TOKEN_STRING") +r, err := client.Service.Operation(auth, args) +``` + +{{/isBasicBearer}} +{{#isBasicBasic}} +- **Type**: HTTP basic authentication + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextBasicAuth, sw.BasicAuth{ + UserName: "username", + Password: "password", +}) +r, err := client.Service.Operation(auth, args) +``` + +{{/isBasicBasic}} +{{#isHttpSignature}} +- **Type**: HTTP signature authentication + +Example + +```golang + authConfig := client.HttpSignatureAuth{ + KeyId: "my-key-id", + PrivateKeyPath: "rsa.pem", + Passphrase: "my-passphrase", + SigningScheme: sw.HttpSigningSchemeHs2019, + SignedHeaders: []string{ + sw.HttpSignatureParameterRequestTarget, // The special (request-target) parameter expresses the HTTP request target. + sw.HttpSignatureParameterCreated, // Time when request was signed, formatted as a Unix timestamp integer value. + "Host", // The Host request header specifies the domain name of the server, and optionally the TCP port number. + "Date", // The date and time at which the message was originated. + "Content-Type", // The Media type of the body of the request. + "Digest", // A cryptographic digest of the request body. + }, + SigningAlgorithm: sw.HttpSigningAlgorithmRsaPSS, + SignatureMaxValidity: 5 * time.Minute, + } + var authCtx context.Context + var err error + if authCtx, err = authConfig.ContextWithValue(context.Background()); err != nil { + // Process error + } + r, err = client.Service.Operation(auth, args) + +``` +{{/isHttpSignature}} +{{/isBasic}} +{{#isOAuth}} + +- **Type**: OAuth +- **Flow**: {{{flow}}} +- **Authorization URL**: {{{authorizationUrl}}} +- **Scopes**: {{^scopes}}N/A{{/scopes}} +{{#scopes}} - **{{{scope}}}**: {{{description}}} +{{/scopes}} + +Example + +```golang +auth := context.WithValue(context.Background(), sw.ContextAccessToken, "ACCESSTOKENSTRING") +r, err := client.Service.Operation(auth, args) +``` + +Or via OAuth2 module to automatically refresh tokens and perform user authentication. + +```golang +import "golang.org/x/oauth2" + +/* Perform OAuth2 round trip request and obtain a token */ + +tokenSource := oauth2cfg.TokenSource(createContext(httpClient), &token) +auth := context.WithValue(oauth2.NoContext, sw.ContextOAuth2, tokenSource) +r, err := client.Service.Operation(auth, args) +``` + +{{/isOAuth}} +{{/authMethods}} + +## Documentation for Utility Methods + +Due to the fact that model structure members are all pointers, this package contains +a number of utility functions to easily obtain pointers to values of basic types. +Each of these functions takes a value of the given basic type and returns a pointer to it: + +* `PtrBool` +* `PtrInt` +* `PtrInt32` +* `PtrInt64` +* `PtrFloat` +* `PtrFloat32` +* `PtrFloat64` +* `PtrString` +* `PtrTime` + +## Author + +{{#apiInfo}}{{#apis}}{{#-last}}{{infoEmail}} +{{/-last}}{{/apis}}{{/apiInfo}} diff --git a/.schema/openapi/templates/go/api.mustache b/.schema/openapi/templates/go/api.mustache new file mode 100644 index 0000000..6ee735a --- /dev/null +++ b/.schema/openapi/templates/go/api.mustache @@ -0,0 +1,377 @@ +{{>partial_header}} +package {{packageName}} + +{{#operations}} +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +{{#imports}} "{{import}}" +{{/imports}} +) + +// Linger please +var ( + _ context.Context +) +{{#generateInterfaces}} + +type {{classname}} interface { + {{#operation}} + + /* + * {{operationId}}{{#summary}} {{{.}}}{{/summary}}{{^summary}} Method for {{operationId}}{{/summary}} + {{#notes}} + * {{{unescapedNotes}}} + {{/notes}} + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}} + * @param {{paramName}}{{#description}} {{{.}}}{{/description}}{{/pathParams}} + * @return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request + */ + {{{nickname}}}(ctx context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request + + /* + * {{nickname}}Execute executes the request{{#returnType}} + * @return {{{.}}}{{/returnType}} + */ + {{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{^isArray}}{{^returnTypeIsPrimitive}}*{{/returnTypeIsPrimitive}}{{/isArray}}{{{.}}}, {{/returnType}}*http.Response, error) + {{/operation}} +} +{{/generateInterfaces}} + +// {{classname}}Service {{classname}} service +type {{classname}}Service service +{{#operation}} + +type {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request struct { + ctx context.Context{{#generateInterfaces}} + ApiService {{classname}} +{{/generateInterfaces}}{{^generateInterfaces}} + ApiService *{{classname}}Service +{{/generateInterfaces}} +{{#allParams}} + {{paramName}} {{^isPathParam}}*{{/isPathParam}}{{{dataType}}} +{{/allParams}} +} +{{#allParams}}{{^isPathParam}} +func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) {{vendorExtensions.x-export-param-name}}({{paramName}} {{{dataType}}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request { + r.{{paramName}} = &{{paramName}} + return r +}{{/isPathParam}}{{/allParams}} + +func (r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) Execute() ({{#returnType}}{{^isArray}}{{^returnTypeIsPrimitive}}*{{/returnTypeIsPrimitive}}{{/isArray}}{{{.}}}, {{/returnType}}*http.Response, error) { + return r.ApiService.{{nickname}}Execute(r) +} + +/* + * {{operationId}}{{#summary}} {{{.}}}{{/summary}}{{^summary}} Method for {{operationId}}{{/summary}} +{{#notes}} + * {{{unescapedNotes}}} +{{/notes}} + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().{{#pathParams}} + * @param {{paramName}}{{#description}} {{{.}}}{{/description}}{{/pathParams}} + * @return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request + */ +func (a *{{{classname}}}Service) {{{nickname}}}(ctx context.Context{{#pathParams}}, {{paramName}} {{{dataType}}}{{/pathParams}}) {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request { + return {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request{ + ApiService: a, + ctx: ctx,{{#pathParams}} + {{paramName}}: {{paramName}},{{/pathParams}} + } +} + +/* + * Execute executes the request{{#returnType}} + * @return {{{.}}}{{/returnType}} + */ +func (a *{{{classname}}}Service) {{nickname}}Execute(r {{#structPrefix}}{{&classname}}{{/structPrefix}}Api{{operationId}}Request) ({{#returnType}}{{^isArray}}{{^returnTypeIsPrimitive}}*{{/returnTypeIsPrimitive}}{{/isArray}}{{{.}}}, {{/returnType}}*http.Response, error) { + var ( + localVarHTTPMethod = http.Method{{httpMethod}} + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + {{#returnType}} + localVarReturnValue {{^isArray}}{{^returnTypeIsPrimitive}}*{{/returnTypeIsPrimitive}}{{/isArray}}{{{.}}} + {{/returnType}} + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "{{{classname}}}Service.{{{nickname}}}") + if err != nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "{{{path}}}"{{#pathParams}} + localVarPath = strings.Replace(localVarPath, "{"+"{{baseName}}"+"}", url.PathEscape(parameterToString(r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")), -1){{/pathParams}} + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + {{#allParams}} + {{#required}} + {{^isPathParam}} + if r.{{paramName}} == nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} is required and must be specified") + } + {{/isPathParam}} + {{#minItems}} + if len({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) < {{minItems}} { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must have at least {{minItems}} elements") + } + {{/minItems}} + {{#maxItems}} + if len({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) > {{maxItems}} { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must have less than {{maxItems}} elements") + } + {{/maxItems}} + {{#minLength}} + if strlen({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) < {{minLength}} { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must have at least {{minLength}} elements") + } + {{/minLength}} + {{#maxLength}} + if strlen({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) > {{maxLength}} { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must have less than {{maxLength}} elements") + } + {{/maxLength}} + {{#minimum}} + {{#isString}} + {{paramName}}Txt, err := atoi({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) + if {{paramName}}Txt < {{minimum}} { + {{/isString}} + {{^isString}} + if {{^isPathParam}}*{{/isPathParam}}r.{{paramName}} < {{minimum}} { + {{/isString}} + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must be greater than {{minimum}}") + } + {{/minimum}} + {{#maximum}} + {{#isString}} + {{paramName}}Txt, err := atoi({{^isPathParam}}*{{/isPathParam}}r.{{paramName}}) + if {{paramName}}Txt > {{maximum}} { + {{/isString}} + {{^isString}} + if {{^isPathParam}}*{{/isPathParam}}r.{{paramName}} > {{maximum}} { + {{/isString}} + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, reportError("{{paramName}} must be less than {{maximum}}") + } + {{/maximum}} + {{/required}} + {{/allParams}} + + {{#queryParams}} + {{#required}} + {{#isCollectionFormatMulti}} + { + t := *r.{{paramName}} + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("{{baseName}}", parameterToString(s.Index(i), "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + } + } else { + localVarQueryParams.Add("{{baseName}}", parameterToString(t, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + } + } + {{/isCollectionFormatMulti}} + {{^isCollectionFormatMulti}} + localVarQueryParams.Add("{{baseName}}", parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + {{/isCollectionFormatMulti}} + {{/required}} + {{^required}} + if r.{{paramName}} != nil { + {{#isCollectionFormatMulti}} + t := *r.{{paramName}} + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("{{baseName}}", parameterToString(s.Index(i), "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + } + } else { + localVarQueryParams.Add("{{baseName}}", parameterToString(t, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + } + {{/isCollectionFormatMulti}} + {{^isCollectionFormatMulti}} + localVarQueryParams.Add("{{baseName}}", parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + {{/isCollectionFormatMulti}} + } + {{/required}} + {{/queryParams}} + // to determine the Content-Type header +{{=<% %>=}} + localVarHTTPContentTypes := []string{<%#consumes%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/consumes%>} +<%={{ }}=%> + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header +{{=<% %>=}} + localVarHTTPHeaderAccepts := []string{<%#produces%>"<%&mediaType%>"<%^-last%>, <%/-last%><%/produces%>} +<%={{ }}=%> + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } +{{#headerParams}} + {{#required}} + localVarHeaderParams["{{baseName}}"] = parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") + {{/required}} + {{^required}} + if r.{{paramName}} != nil { + localVarHeaderParams["{{baseName}}"] = parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}") + } + {{/required}} +{{/headerParams}} +{{#formParams}} +{{#isFile}} + localVarFormFileName = "{{baseName}}" +{{#required}} + localVarFile := *r.{{paramName}} +{{/required}} +{{^required}} + var localVarFile {{dataType}} + if r.{{paramName}} != nil { + localVarFile = *r.{{paramName}} + } +{{/required}} + if localVarFile != nil { + fbs, _ := io.ReadAll(localVarFile) + localVarFileBytes = fbs + localVarFileName = localVarFile.Name() + localVarFile.Close() + } +{{/isFile}} +{{^isFile}} +{{#required}} + localVarFormParams.Add("{{baseName}}", parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) +{{/required}} +{{^required}} +{{#isModel}} + if r.{{paramName}} != nil { + paramJson, err := parameterToJson(*r.{{paramName}}) + if err != nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, err + } + localVarFormParams.Add("{{baseName}}", paramJson) + } +{{/isModel}} +{{^isModel}} + if r.{{paramName}} != nil { + localVarFormParams.Add("{{baseName}}", parameterToString(*r.{{paramName}}, "{{#collectionFormat}}{{collectionFormat}}{{/collectionFormat}}")) + } +{{/isModel}} +{{/required}} +{{/isFile}} +{{/formParams}} +{{#bodyParams}} + // body params + localVarPostBody = r.{{paramName}} +{{/bodyParams}} +{{#authMethods}} +{{#isApiKey}} +{{^isKeyInCookie}} + if r.ctx != nil { + // API Key Authentication + if auth, ok := r.ctx.Value(ContextAPIKeys).(map[string]APIKey); ok { + {{#vendorExtensions.x-auth-id-alias}} + if apiKey, ok := auth["{{.}}"]; ok { + var key string + if prefix, ok := auth["{{name}}"]; ok && prefix.Prefix != "" { + key = prefix.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + {{/vendorExtensions.x-auth-id-alias}} + {{^vendorExtensions.x-auth-id-alias}} + if apiKey, ok := auth["{{name}}"]; ok { + var key string + if apiKey.Prefix != "" { + key = apiKey.Prefix + " " + apiKey.Key + } else { + key = apiKey.Key + } + {{/vendorExtensions.x-auth-id-alias}} + {{#isKeyInHeader}} + localVarHeaderParams["{{keyParamName}}"] = key + {{/isKeyInHeader}} + {{#isKeyInQuery}} + localVarQueryParams.Add("{{keyParamName}}", key) + {{/isKeyInQuery}} + } + } + } +{{/isKeyInCookie}} +{{/isApiKey}} +{{/authMethods}} + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + {{#responses}} + {{#dataType}} + {{^is1xx}} + {{^is2xx}} + {{^wildcard}} + if localVarHTTPResponse.StatusCode == {{{code}}} { + {{/wildcard}} + var v {{{dataType}}} + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr + } + newErr.model = v + {{^-last}} + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr + {{/-last}} + {{^wildcard}} + } + {{/wildcard}} + {{/is2xx}} + {{/is1xx}} + {{/dataType}} + {{/responses}} + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr + } + + {{#returnType}} + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, newErr + } + + {{/returnType}} + return {{#returnType}}localVarReturnValue, {{/returnType}}localVarHTTPResponse, nil +} +{{/operation}} +{{/operations}} diff --git a/.schema/openapi/templates/go/api_doc.mustache b/.schema/openapi/templates/go/api_doc.mustache new file mode 100644 index 0000000..3e7c11b --- /dev/null +++ b/.schema/openapi/templates/go/api_doc.mustache @@ -0,0 +1,92 @@ +# {{invokerPackage}}\{{classname}}{{#description}} + +{{description}}{{/description}} + +All URIs are relative to *{{basePath}}* + +Method | HTTP request | Description +------------- | ------------- | ------------- +{{#operations}}{{#operation}}[**{{operationId}}**]({{classname}}.md#{{operationId}}) | **{{httpMethod}}** {{path}} | {{#summary}}{{summary}}{{/summary}} +{{/operation}}{{/operations}} + +{{#operations}} +{{#operation}} + +## {{{operationId}}} + +> {{#returnType}}{{{.}}} {{/returnType}}{{{operationId}}}(ctx{{#pathParams}}, {{paramName}}{{/pathParams}}){{#allParams}}{{^isPathParam}}.{{vendorExtensions.x-export-param-name}}({{paramName}}){{/isPathParam}}{{/allParams}}.Execute() + +{{{summary}}}{{#notes}} + +{{{unespacedNotes}}}{{/notes}} + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" +{{#vendorExtensions.x-go-import}} +{{{vendorExtensions.x-go-import}}} +{{/vendorExtensions.x-go-import}} + {{goImportAlias}} "./openapi" +) + +func main() { + {{#allParams}} + {{paramName}} := {{{vendorExtensions.x-go-example}}} // {{{dataType}}} | {{{description}}}{{^required}} (optional){{/required}}{{#defaultValue}} (default to {{{.}}}){{/defaultValue}} + {{/allParams}} + + configuration := {{goImportAlias}}.NewConfiguration() + apiClient := {{goImportAlias}}.NewAPIClient(configuration) + resp, r, err := apiClient.{{classname}}.{{operationId}}(context.Background(){{#pathParams}}, {{paramName}}{{/pathParams}}){{#allParams}}{{^isPathParam}}.{{vendorExtensions.x-export-param-name}}({{paramName}}){{/isPathParam}}{{/allParams}}.Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `{{classname}}.{{operationId}}``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + {{#returnType}} + // response from `{{operationId}}`: {{{.}}} + fmt.Fprintf(os.Stdout, "Response from `{{classname}}.{{operationId}}`: %v\n", resp) + {{/returnType}} +} +``` + +### Path Parameters + +{{^allParams}}This endpoint does not need any parameter.{{/allParams}}{{#pathParams}}{{#-last}} +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.{{/-last}}{{/pathParams}}{{#pathParams}} +**{{paramName}}** | {{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{baseType}}.md){{/isFile}}{{/isPrimitiveType}} | {{description}} | {{#defaultValue}}[default to {{defaultValue}}]{{/defaultValue}}{{/pathParams}} + +### Other Parameters + +Other parameters are passed through a pointer to a api{{{nickname}}}Request struct via the builder pattern +{{#allParams}}{{#-last}} + +Name | Type | Description | Notes +------------- | ------------- | ------------- | -------------{{/-last}}{{/allParams}}{{#allParams}} +{{^isPathParam}} **{{paramName}}** | {{#isContainer}}{{#isArray}}{{#items}}{{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**[]{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isFile}}{{/isPrimitiveType}}{{/items}}{{/isArray}}{{#isMap}}{{#items}}{{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**map[string]{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isFile}}{{/isPrimitiveType}}{{/items}}{{/isMap}}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isFile}}{{/isPrimitiveType}}{{/isContainer}} | {{description}} | {{#defaultValue}}[default to {{defaultValue}}]{{/defaultValue}}{{/isPathParam}}{{/allParams}} + +### Return type + +{{#returnType}}{{#returnTypeIsPrimitive}}**{{{returnType}}}**{{/returnTypeIsPrimitive}}{{^returnTypeIsPrimitive}}[**{{{returnType}}}**]({{returnBaseType}}.md){{/returnTypeIsPrimitive}}{{/returnType}}{{^returnType}} (empty response body){{/returnType}} + +### Authorization + +{{^authMethods}}No authorization required{{/authMethods}}{{#authMethods}}[{{{name}}}](../README.md#{{{name}}}){{^-last}}, {{/-last}}{{/authMethods}} + +### HTTP request headers + +- **Content-Type**: {{#consumes}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/consumes}}{{^consumes}}Not defined{{/consumes}} +- **Accept**: {{#produces}}{{{mediaType}}}{{^-last}}, {{/-last}}{{/produces}}{{^produces}}Not defined{{/produces}} + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + +{{/operation}} +{{/operations}} diff --git a/.schema/openapi/templates/go/client.mustache b/.schema/openapi/templates/go/client.mustache new file mode 100644 index 0000000..73fa055 --- /dev/null +++ b/.schema/openapi/templates/go/client.mustache @@ -0,0 +1,583 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" + + "golang.org/x/oauth2" + {{#withAWSV4Signature}} + awsv4 "github.com/aws/aws-sdk-go/aws/signer/v4" + awscredentials "github.com/aws/aws-sdk-go/aws/credentials" + {{/withAWSV4Signature}} +) + +var ( + jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) + xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) +) + +// APIClient manages communication with the {{appName}} API v{{version}} +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services +{{#apiInfo}} +{{#apis}} +{{#operations}} + + {{#generateInterfaces}} + {{classname}} {{classname}} + {{/generateInterfaces}} + {{^generateInterfaces}} + {{classname}} *{{classname}}Service + {{/generateInterfaces}} +{{/operations}} +{{/apis}} +{{/apiInfo}} +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + c.common.client = c + +{{#apiInfo}} + // API Services +{{#apis}} +{{#operations}} + c.{{classname}} = (*{{classname}}Service)(&c.common) +{{/operations}} +{{/apis}} +{{/apiInfo}} + + return c +} + +func atoi(in string) (int, error) { + return strconv.Atoi(in) +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insenstive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.ToLower(a) == strings.ToLower(needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("Expected %s to be of type %s but received %s.", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +// parameterToString convert interface{} parameters to string, using a delimiter if format is provided. +func parameterToString(obj interface{}, collectionFormat string) string { + var delimiter string + + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") + } else if t, ok := obj.(time.Time); ok { + return t.Format(time.RFC3339) + } + + return fmt.Sprintf("%v", obj) +} + +// helper for converting interface{} parameters to json strings +func parameterToJson(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + + +// callAPI do the request. +func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + return resp, err +} + +// Allow modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams url.Values, + formFileName string, + fileName string, + fileBytes []byte) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // add form parameters and file if available. + if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(fileBytes) > 0 && fileName != "") { + if body != nil { + return nil, errors.New("Cannot specify postBody and multipart form at the same time.") + } + body = &bytes.Buffer{} + w := multipart.NewWriter(body) + + for k, v := range formParams { + for _, iv := range v { + if strings.HasPrefix(k, "@") { // file + err = addFile(w, k[1:], iv) + if err != nil { + return nil, err + } + } else { // form value + w.WriteField(k, iv) + } + } + } + if len(fileBytes) > 0 && fileName != "" { + w.Boundary() + //_, fileNm := filepath.Split(fileName) + part, err := w.CreateFormFile(formFileName, filepath.Base(fileName)) + if err != nil { + return nil, err + } + _, err = part.Write(fileBytes) + if err != nil { + return nil, err + } + } + + // Set the Boundary in the Content-Type + headerParams["Content-Type"] = w.FormDataContentType() + + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + w.Close() + } + + if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { + if body != nil { + return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") + } + body = &bytes.Buffer{} + body.WriteString(formParams.Encode()) + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Adding Query Param + query := url.Query() + for k, v := range queryParams { + for _, iv := range v { + query.Add(k, iv) + } + } + + // Encode the parameters. + url.RawQuery = query.Encode() + + // Generate a new request + if body != nil { + localVarRequest, err = http.NewRequest(method, url.String(), body) + } else { + localVarRequest, err = http.NewRequest(method, url.String(), nil) + } + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers.Set(h, v) + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + + // Walk through any authentication. + + // OAuth2 authentication + if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok { + // We were able to grab an oauth2 token from the context + var latestToken *oauth2.Token + if latestToken, err = tok.Token(); err != nil { + return nil, err + } + + latestToken.SetAuthHeader(localVarRequest) + } + + // Basic HTTP Authentication + if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok { + localVarRequest.SetBasicAuth(auth.UserName, auth.Password) + } + + // AccessToken Authentication + if auth, ok := ctx.Value(ContextAccessToken).(string); ok { + localVarRequest.Header.Add("Authorization", "Bearer "+auth) + } + + {{#withAWSV4Signature}} + // AWS Signature v4 Authentication + if auth, ok := ctx.Value(ContextAWSv4).(AWSv4); ok { + creds := awscredentials.NewStaticCredentials(auth.AccessKey, auth.SecretKey, "") + signer := awsv4.NewSigner(creds) + var reader *strings.Reader + if body == nil { + reader = strings.NewReader("") + } else { + reader = strings.NewReader(body.String()) + } + timestamp := time.Now() + _, err := signer.Sign(localVarRequest, reader, "oapi", "eu-west-2", timestamp) + if err != nil { + return nil, err + } + } + {{/withAWSV4Signature}} + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } +{{#hasHttpSignatureMethods}} + if ctx != nil { + // HTTP Signature Authentication. All request headers must be set (including default headers) + // because the headers may be included in the signature. + if auth, ok := ctx.Value(ContextHttpSignatureAuth).(HttpSignatureAuth); ok { + err = SignRequest(ctx, localVarRequest, auth) + if err != nil { + return nil, err + } + } + } +{{/hasHttpSignatureMethods}} + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if xmlCheck.MatchString(contentType) { + if err = xml.Unmarshal(b, v); err != nil { + return err + } + return nil + } + if jsonCheck.MatchString(contentType) { + if actualObj, ok := v.(interface{GetActualInstance() interface{}}); ok { // oneOf, anyOf schemas + if unmarshalObj, ok := actualObj.(interface{UnmarshalJSON([]byte) error}); ok { // make sure it has UnmarshalJSON defined + if err = unmarshalObj.UnmarshalJSON(b); err!= nil { + return err + } + } else { + return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") + } + } else if err = json.Unmarshal(b, v); err != nil { // simple model + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Add a file to the multipart request +func addFile(w *multipart.Writer, fieldName, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + part, err := w.CreateFormFile(fieldName, filepath.Base(path)) + if err != nil { + return err + } + _, err = io.Copy(part, file) + + return err +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} + +// Prevent trying to import "bytes" +func newStrictDecoder(data []byte) *json.Decoder { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.DisallowUnknownFields() + return dec +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if jsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } else if xmlCheck.MatchString(contentType) { + err = xml.NewEncoder(bodyBuf).Encode(body) + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("Invalid body type %s\n", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + return contentType +} + +// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go +type cacheControl map[string]string + +func parseCacheControl(headers http.Header) cacheControl { + cc := cacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc +} + +// CacheExpires helper function to determine remaining time before repeating a request. +func CacheExpires(r *http.Response) time.Time { + // Figure out when the cache expires. + var expires time.Time + now, err := time.Parse(time.RFC1123, r.Header.Get("date")) + if err != nil { + return time.Now() + } + respCacheControl := parseCacheControl(r.Header) + + if maxAge, ok := respCacheControl["max-age"]; ok { + lifetime, err := time.ParseDuration(maxAge + "s") + if err != nil { + expires = now + } else { + expires = now.Add(lifetime) + } + } else { + expiresHeader := r.Header.Get("Expires") + if expiresHeader != "" { + expires, err = time.Parse(time.RFC1123, expiresHeader) + if err != nil { + expires = now + } + } + } + return expires +} + +func strlen(s string) int { + return utf8.RuneCountInString(s) +} + +// GenericOpenAPIError Provides access to the body, error and model on returned errors. +type GenericOpenAPIError struct { + body []byte + error string + model interface{} +} + +// Error returns non-empty string if there was an error. +func (e GenericOpenAPIError) Error() string { + return e.error +} + +// Body returns the raw bytes of the response +func (e GenericOpenAPIError) Body() []byte { + return e.body +} + +// Model returns the unpacked model of the error +func (e GenericOpenAPIError) Model() interface{} { + return e.model +} diff --git a/.schema/openapi/templates/go/configuration.mustache b/.schema/openapi/templates/go/configuration.mustache new file mode 100644 index 0000000..1f5436d --- /dev/null +++ b/.schema/openapi/templates/go/configuration.mustache @@ -0,0 +1,303 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "context" + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request. + ContextOAuth2 = contextKey("token") + + // ContextBasicAuth takes BasicAuth as authentication for the request. + ContextBasicAuth = contextKey("basic") + + // ContextAccessToken takes a string oauth2 access token as authentication for the request. + ContextAccessToken = contextKey("accesstoken") + + // ContextAPIKeys takes a string apikey as authentication for the request + ContextAPIKeys = contextKey("apiKeys") + + {{#withAWSV4Signature}} + // ContextAWSv4 takes an Access Key and a Secret Key for signing AWS Signature v4 + ContextAWSv4 = contextKey("awsv4") + + {{/withAWSV4Signature}} + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") + + // ContextServerIndex uses a server configuration from the index. + ContextServerIndex = contextKey("serverIndex") + + // ContextOperationServerIndices uses a server configuration from the index mapping. + ContextOperationServerIndices = contextKey("serverOperationIndices") + + // ContextServerVariables overrides a server configuration variables. + ContextServerVariables = contextKey("serverVariables") + + // ContextOperationServerVariables overrides a server configuration variables using operation specific values. + ContextOperationServerVariables = contextKey("serverOperationVariables") +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +{{#withAWSV4Signature}} +// AWSv4 provides AWS Signature to a request passed via context using ContextAWSv4 +// https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html +type AWSv4 struct { + AccessKey string + SecretKey string +} + +{{/withAWSV4Signature}} +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + URL string + Description string + Variables map[string]ServerVariable +} + +// ServerConfigurations stores multiple ServerConfiguration items +type ServerConfigurations []ServerConfiguration + +// Configuration stores the configuration of the API client +type Configuration struct { + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers ServerConfigurations + OperationServers map[string]ServerConfigurations + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration() *Configuration { + cfg := &Configuration{ + DefaultHeader: make(map[string]string), + UserAgent: "{{#httpUserAgent}}{{{.}}}{{/httpUserAgent}}{{^httpUserAgent}}OpenAPI-Generator/{{{packageVersion}}}/go{{/httpUserAgent}}", + Debug: false, + {{#servers}} + {{#-first}} + Servers: ServerConfigurations{ + {{/-first}} + { + URL: "{{{url}}}", + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + Variables: map[string]ServerVariable{ + {{/-first}} + "{{{name}}}": ServerVariable{ + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + DefaultValue: "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + EnumValues: []string{ + {{/-first}} + "{{{.}}}", + {{#-last}} + }, + {{/-last}} + {{/enumValues}} + }, + {{#-last}} + }, + {{/-last}} + {{/variables}} + }, + {{#-last}} + }, + {{/-last}} + {{/servers}} + {{#apiInfo}} + OperationServers: map[string]ServerConfigurations{ + {{#apis}} + {{#operations}} + {{#operation}} + {{#servers}} + {{#-first}} + "{{{classname}}}Service.{{{nickname}}}": { + {{/-first}} + { + URL: "{{{url}}}", + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + {{#variables}} + {{#-first}} + Variables: map[string]ServerVariable{ + {{/-first}} + "{{{name}}}": ServerVariable{ + Description: "{{{description}}}{{^description}}No description provided{{/description}}", + DefaultValue: "{{{defaultValue}}}", + {{#enumValues}} + {{#-first}} + EnumValues: []string{ + {{/-first}} + "{{{.}}}", + {{#-last}} + }, + {{/-last}} + {{/enumValues}} + }, + {{#-last}} + }, + {{/-last}} + {{/variables}} + }, + {{#-last}} + }, + {{/-last}} + {{/servers}} + {{/operation}} + {{/operations}} + {{/apis}} + }, + {{/apiInfo}} + } + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// URL formats template on a index using given variables +func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { + if index < 0 || len(sc) <= index { + return "", fmt.Errorf("Index %v out of range %v", index, len(sc)-1) + } + server := sc[index] + url := server.URL + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("The variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} + +// ServerURL returns URL based on server settings +func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { + return c.Servers.URL(index, variables) +} + +func getServerIndex(ctx context.Context) (int, error) { + si := ctx.Value(ContextServerIndex) + if si != nil { + if index, ok := si.(int); ok { + return index, nil + } + return 0, reportError("Invalid type %T should be int", si) + } + return 0, nil +} + +func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { + osi := ctx.Value(ContextOperationServerIndices) + if osi != nil { + if operationIndices, ok := osi.(map[string]int); !ok { + return 0, reportError("Invalid type %T should be map[string]int", osi) + } else { + index, ok := operationIndices[endpoint] + if ok { + return index, nil + } + } + } + return getServerIndex(ctx) +} + +func getServerVariables(ctx context.Context) (map[string]string, error) { + sv := ctx.Value(ContextServerVariables) + if sv != nil { + if variables, ok := sv.(map[string]string); ok { + return variables, nil + } + return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) + } + return nil, nil +} + +func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { + osv := ctx.Value(ContextOperationServerVariables) + if osv != nil { + if operationVariables, ok := osv.(map[string]map[string]string); !ok { + return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) + } else { + variables, ok := operationVariables[endpoint] + if ok { + return variables, nil + } + } + } + return getServerVariables(ctx) +} + +// ServerURLWithContext returns a new server URL given an endpoint +func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { + sc, ok := c.OperationServers[endpoint] + if !ok { + sc = c.Servers + } + + if ctx == nil { + return sc.URL(0, nil) + } + + index, err := getServerOperationIndex(ctx, endpoint) + if err != nil { + return "", err + } + + variables, err := getServerOperationVariables(ctx, endpoint) + if err != nil { + return "", err + } + + return sc.URL(index, variables) +} diff --git a/.schema/openapi/templates/go/git_push.sh.mustache b/.schema/openapi/templates/go/git_push.sh.mustache new file mode 100755 index 0000000..8b3f689 --- /dev/null +++ b/.schema/openapi/templates/go/git_push.sh.mustache @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="{{{gitHost}}}" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="{{{gitUserId}}}" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="{{{gitRepoId}}}" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="{{{releaseNote}}}" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/.schema/openapi/templates/go/gitignore.mustache b/.schema/openapi/templates/go/gitignore.mustache new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/.schema/openapi/templates/go/gitignore.mustache @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/.schema/openapi/templates/go/model.mustache b/.schema/openapi/templates/go/model.mustache new file mode 100644 index 0000000..684af1d --- /dev/null +++ b/.schema/openapi/templates/go/model.mustache @@ -0,0 +1,20 @@ +{{>partial_header}} +package {{packageName}} + +{{#models}} +import ( + "encoding/json" +{{#imports}} + "{{import}}" +{{/imports}} +) + +{{#model}} +{{#isEnum}} +{{>model_enum}} +{{/isEnum}} +{{^isEnum}} +{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>model_anyof}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>model_simple}}{{/anyOf}}{{/oneOf}} +{{/isEnum}} +{{/model}} +{{/models}} diff --git a/.schema/openapi/templates/go/model_anyof.mustache b/.schema/openapi/templates/go/model_anyof.mustache new file mode 100644 index 0000000..5dfa753 --- /dev/null +++ b/.schema/openapi/templates/go/model_anyof.mustache @@ -0,0 +1,76 @@ +// {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}} +type {{classname}} struct { + {{#anyOf}} + {{{.}}} *{{{.}}} + {{/anyOf}} +} + +// Unmarshal JSON data into any of the pointers in the struct +func (dst *{{classname}}) UnmarshalJSON(data []byte) error { + var err error + {{#isNullable}} + // this object is nullable so check if the payload is null or empty string + if string(data) == "" || string(data) == "{}" { + return nil + } + + {{/isNullable}} + {{#discriminator}} + {{#mappedModels}} + {{#-first}} + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err := json.Unmarshal(data, &jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + {{/-first}} + // check if the discriminator value is '{{{mappingName}}}' + if jsonDict["{{{propertyBaseName}}}"] == "{{{mappingName}}}" { + // try to unmarshal JSON data into {{{modelName}}} + err = json.Unmarshal(data, &dst.{{{modelName}}}); + if err == nil { + json{{{modelName}}}, _ := json.Marshal(dst.{{{modelName}}}) + if string(json{{{modelName}}}) == "{}" { // empty struct + dst.{{{modelName}}} = nil + } else { + return nil // data stored in dst.{{{modelName}}}, return on the first match + } + } else { + dst.{{{modelName}}} = nil + } + } + + {{/mappedModels}} + {{/discriminator}} + {{#anyOf}} + // try to unmarshal JSON data into {{{.}}} + err = json.Unmarshal(data, &dst.{{{.}}}); + if err == nil { + json{{{.}}}, _ := json.Marshal(dst.{{{.}}}) + if string(json{{{.}}}) == "{}" { // empty struct + dst.{{{.}}} = nil + } else { + return nil // data stored in dst.{{{.}}}, return on the first match + } + } else { + dst.{{{.}}} = nil + } + + {{/anyOf}} + return fmt.Errorf("Data failed to match schemas in anyOf({{classname}})") +} + +// Marshal data from the first non-nil pointers in the struct to JSON +func (src *{{classname}}) MarshalJSON() ([]byte, error) { +{{#anyOf}} + if src.{{{.}}} != nil { + return json.Marshal(&src.{{{.}}}) + } + +{{/anyOf}} + return nil, nil // no data in anyOf schemas +} + +{{>nullable_model}} diff --git a/.schema/openapi/templates/go/model_doc.mustache b/.schema/openapi/templates/go/model_doc.mustache new file mode 100644 index 0000000..439e695 --- /dev/null +++ b/.schema/openapi/templates/go/model_doc.mustache @@ -0,0 +1,97 @@ +{{#models}}{{#model}}# {{classname}} + +{{^isEnum}} +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +{{#vendorExtensions.x-is-one-of-interface}} +**{{classname}}Interface** | **interface { {{#discriminator}}{{propertyGetter}}() {{propertyType}}{{/discriminator}} }** | An interface that can hold any of the proper implementing types | +{{/vendorExtensions.x-is-one-of-interface}} +{{^vendorExtensions.x-is-one-of-interface}} +{{#vars}}**{{name}}** | {{^required}}Pointer to {{/required}}{{#isContainer}}{{#isArray}}{{#items}}{{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**[]{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{dataType}}.md){{/isFile}}{{/isPrimitiveType}}{{/items}}{{/isArray}}{{#isMap}}{{#items}}{{^isPrimitiveType}}{{^isFile}}[{{/isFile}}{{/isPrimitiveType}}**map[string]{{dataType}}**{{^isPrimitiveType}}{{^isFile}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isFile}}{{/isPrimitiveType}}{{/items}}{{/isMap}}{{/isContainer}}{{^isContainer}}{{^isPrimitiveType}}{{^isFile}}{{^isDateTime}}[{{/isDateTime}}{{/isFile}}{{/isPrimitiveType}}**{{dataType}}**{{^isPrimitiveType}}{{^isFile}}{{^isDateTime}}]({{^baseType}}{{dataType}}{{/baseType}}{{baseType}}.md){{/isDateTime}}{{/isFile}}{{/isPrimitiveType}}{{/isContainer}} | {{description}} | {{^required}}[optional] {{/required}}{{#isReadOnly}}[readonly] {{/isReadOnly}}{{#defaultValue}}[default to {{{.}}}]{{/defaultValue}} +{{/vars}} +{{/vendorExtensions.x-is-one-of-interface}} + +## Methods + +{{^vendorExtensions.x-is-one-of-interface}} +### New{{classname}} + +`func New{{classname}}({{#vars}}{{#required}}{{nameInCamelCase}} {{dataType}}, {{/required}}{{/vars}}) *{{classname}}` + +New{{classname}} instantiates a new {{classname}} object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### New{{classname}}WithDefaults + +`func New{{classname}}WithDefaults() *{{classname}}` + +New{{classname}}WithDefaults instantiates a new {{classname}} object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +{{#vars}} +### Get{{name}} + +`func (o *{{classname}}) Get{{name}}() {{vendorExtensions.x-go-base-type}}` + +Get{{name}} returns the {{name}} field if non-nil, zero value otherwise. + +### Get{{name}}Ok + +`func (o *{{classname}}) Get{{name}}Ok() (*{{vendorExtensions.x-go-base-type}}, bool)` + +Get{{name}}Ok returns a tuple with the {{name}} field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### Set{{name}} + +`func (o *{{classname}}) Set{{name}}(v {{vendorExtensions.x-go-base-type}})` + +Set{{name}} sets {{name}} field to given value. + +{{^required}} +### Has{{name}} + +`func (o *{{classname}}) Has{{name}}() bool` + +Has{{name}} returns a boolean if a field has been set. +{{/required}} + +{{#isNullable}} +### Set{{name}}Nil + +`func (o *{{classname}}) Set{{name}}Nil(b bool)` + + Set{{name}}Nil sets the value for {{name}} to be an explicit nil + +### Unset{{name}} +`func (o *{{classname}}) Unset{{name}}()` + +Unset{{name}} ensures that no value is present for {{name}}, not even an explicit nil +{{/isNullable}} +{{/vars}} +{{#vendorExtensions.x-implements}} + +### As{{{.}}} + +`func (s *{{classname}}) As{{{.}}}() {{{.}}}` + +Convenience method to wrap this instance of {{classname}} in {{{.}}} +{{/vendorExtensions.x-implements}} +{{/vendorExtensions.x-is-one-of-interface}} +{{/isEnum}} +{{#isEnum}} +## Enum + +{{#allowableValues}}{{#enumVars}} +* `{{name}}` (value: `{{{value}}}`) +{{/enumVars}}{{/allowableValues}} +{{/isEnum}} + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + +{{/model}}{{/models}} diff --git a/.schema/openapi/templates/go/model_enum.mustache b/.schema/openapi/templates/go/model_enum.mustache new file mode 100644 index 0000000..1d3c224 --- /dev/null +++ b/.schema/openapi/templates/go/model_enum.mustache @@ -0,0 +1,71 @@ +// {{{classname}}} {{#description}}{{{.}}}{{/description}}{{^description}}the model '{{{classname}}}'{{/description}} +type {{{classname}}} {{^format}}{{dataType}}{{/format}}{{#format}}{{{format}}}{{/format}} + +// List of {{{name}}} +const ( + {{#allowableValues}} + {{#enumVars}} + {{^-first}} + {{/-first}} + {{#enumClassPrefix}}{{{classname.toUpperCase}}}_{{/enumClassPrefix}}{{name}} {{{classname}}} = {{{value}}} + {{/enumVars}} + {{/allowableValues}} +) + +func (v *{{{classname}}}) UnmarshalJSON(src []byte) error { + var value {{^format}}{{dataType}}{{/format}}{{#format}}{{{format}}}{{/format}} + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := {{{classname}}}(value) + for _, existing := range []{{classname}}{ {{#allowableValues}}{{#enumVars}}{{{value}}}, {{/enumVars}} {{/allowableValues}} } { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid {{classname}}", value) +} + +// Ptr returns reference to {{{name}}} value +func (v {{{classname}}}) Ptr() *{{{classname}}} { + return &v +} + +type Nullable{{{classname}}} struct { + value *{{{classname}}} + isSet bool +} + +func (v Nullable{{classname}}) Get() *{{classname}} { + return v.value +} + +func (v *Nullable{{classname}}) Set(val *{{classname}}) { + v.value = val + v.isSet = true +} + +func (v Nullable{{classname}}) IsSet() bool { + return v.isSet +} + +func (v *Nullable{{classname}}) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullable{{classname}}(val *{{classname}}) *Nullable{{classname}} { + return &Nullable{{classname}}{value: val, isSet: true} +} + +func (v Nullable{{{classname}}}) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *Nullable{{{classname}}}) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/.schema/openapi/templates/go/model_oneof.mustache b/.schema/openapi/templates/go/model_oneof.mustache new file mode 100644 index 0000000..cc4960b --- /dev/null +++ b/.schema/openapi/templates/go/model_oneof.mustache @@ -0,0 +1,114 @@ +// {{classname}} - {{#description}}{{{description}}}{{/description}}{{^description}}struct for {{{classname}}}{{/description}} +type {{classname}} struct { + {{#oneOf}} + {{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} *{{{.}}} + {{/oneOf}} +} + +{{#oneOf}} +// {{{.}}}As{{classname}} is a convenience function that returns {{{.}}} wrapped in {{classname}} +func {{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}As{{classname}}(v *{{{.}}}) {{classname}} { + return {{classname}}{ + {{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}: v, + } +} + +{{/oneOf}} + +// Unmarshal JSON data into one of the pointers in the struct +func (dst *{{classname}}) UnmarshalJSON(data []byte) error { + var err error + {{#isNullable}} + // this object is nullable so check if the payload is null or empty string + if string(data) == "" || string(data) == "{}" { + return nil + } + + {{/isNullable}} + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + {{#mappedModels}} + {{#-first}} + // use discriminator value to speed up the lookup + var jsonDict map[string]interface{} + err = newStrictDecoder(data).Decode(&jsonDict) + if err != nil { + return fmt.Errorf("Failed to unmarshal JSON into map for the discrimintor lookup.") + } + + {{/-first}} + // check if the discriminator value is '{{{mappingName}}}' + if jsonDict["{{{propertyBaseName}}}"] == "{{{mappingName}}}" { + // try to unmarshal JSON data into {{{modelName}}} + err = json.Unmarshal(data, &dst.{{{modelName}}}) + if err == nil { + return nil // data stored in dst.{{{modelName}}}, return on the first match + } else { + dst.{{{modelName}}} = nil + return fmt.Errorf("Failed to unmarshal {{classname}} as {{{modelName}}}: %s", err.Error()) + } + } + + {{/mappedModels}} + {{/discriminator}} + return nil + {{/useOneOfDiscriminatorLookup}} + {{^useOneOfDiscriminatorLookup}} + match := 0 + {{#oneOf}} + // try to unmarshal data into {{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} + err = newStrictDecoder(data).Decode(&dst.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}) + if err == nil { + json{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}, _ := json.Marshal(dst.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}) + if string(json{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}) == "{}" { // empty struct + dst.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} = nil + } else { + match++ + } + } else { + dst.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} = nil + } + + {{/oneOf}} + if match > 1 { // more than 1 match + // reset to nil + {{#oneOf}} + dst.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} = nil + {{/oneOf}} + + return fmt.Errorf("Data matches more than one schema in oneOf({{classname}})") + } else if match == 1 { + return nil // exactly one match + } else { // no match + return fmt.Errorf("Data failed to match schemas in oneOf({{classname}})") + } + {{/useOneOfDiscriminatorLookup}} +} + +// Marshal data from the first non-nil pointers in the struct to JSON +func (src {{classname}}) MarshalJSON() ([]byte, error) { +{{#oneOf}} + if src.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} != nil { + return json.Marshal(&src.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}}) + } + +{{/oneOf}} + return nil, nil // no data in oneOf schemas +} + +// Get the actual instance +func (obj *{{classname}}) GetActualInstance() (interface{}) { + if obj == nil { + return nil + } +{{#oneOf}} + if obj.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} != nil { + return obj.{{#lambda.titlecase}}{{{.}}}{{/lambda.titlecase}} + } + +{{/oneOf}} + // all schemas are nil + return nil +} + +{{>nullable_model}} diff --git a/.schema/openapi/templates/go/model_simple.mustache b/.schema/openapi/templates/go/model_simple.mustache new file mode 100644 index 0000000..74bd545 --- /dev/null +++ b/.schema/openapi/templates/go/model_simple.mustache @@ -0,0 +1,391 @@ +// {{classname}}{{#description}} {{{description}}}{{/description}}{{^description}} struct for {{{classname}}}{{/description}} +type {{classname}} struct { +{{#parent}} +{{^isMap}} +{{^isArray}} + {{{parent}}} +{{/isArray}} +{{/isMap}} +{{#isArray}} + Items {{{parent}}} +{{/isArray}} +{{/parent}} +{{#vars}} +{{^-first}} +{{/-first}} +{{#description}} + // {{{description}}} +{{/description}} + {{name}} {{^required}}{{^isNullable}}{{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{/isNullable}}{{/required}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"{{#withXml}} xml:"{{baseName}}{{#isXmlAttribute}},attr{{/isXmlAttribute}}"{{/withXml}}{{#vendorExtensions.x-go-custom-tag}} {{{.}}}{{/vendorExtensions.x-go-custom-tag}}` +{{/vars}} +{{#isAdditionalPropertiesTrue}} + AdditionalProperties map[string]interface{} +{{/isAdditionalPropertiesTrue}} +} + +{{#isAdditionalPropertiesTrue}} +type _{{{classname}}} {{{classname}}} + +{{/isAdditionalPropertiesTrue}} +// New{{classname}} instantiates a new {{classname}} object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func New{{classname}}({{#vars}}{{#required}}{{nameInCamelCase}} {{dataType}}, {{/required}}{{/vars}}) *{{classname}} { + this := {{classname}}{} +{{#vars}} +{{#required}} + this.{{name}} = {{nameInCamelCase}} +{{/required}} +{{^required}} +{{#defaultValue}} +{{^vendorExtensions.x-golang-is-container}} +{{#isNullable}} + var {{nameInCamelCase}} {{{datatypeWithEnum}}} = {{{.}}} + this.{{name}} = *New{{{dataType}}}(&{{nameInCamelCase}}) +{{/isNullable}} +{{^isNullable}} + var {{nameInCamelCase}} {{{dataType}}} = {{{.}}} + this.{{name}} = &{{nameInCamelCase}} +{{/isNullable}} +{{/vendorExtensions.x-golang-is-container}} +{{/defaultValue}} +{{/required}} +{{/vars}} + return &this +} + +// New{{classname}}WithDefaults instantiates a new {{classname}} object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func New{{classname}}WithDefaults() *{{classname}} { + this := {{classname}}{} +{{#vars}} +{{#defaultValue}} +{{^vendorExtensions.x-golang-is-container}} +{{#isNullable}} +{{!we use datatypeWithEnum here, since it will represent the non-nullable name of the datatype, e.g. int64 for NullableInt64}} + var {{nameInCamelCase}} {{{datatypeWithEnum}}} = {{{.}}} + this.{{name}} = *New{{{dataType}}}(&{{nameInCamelCase}}) +{{/isNullable}} +{{^isNullable}} + var {{nameInCamelCase}} {{{dataType}}} = {{{.}}} + this.{{name}} = {{^required}}&{{/required}}{{nameInCamelCase}} +{{/isNullable}} +{{/vendorExtensions.x-golang-is-container}} +{{/defaultValue}} +{{/vars}} + return &this +} + +{{#vars}} +{{#required}} +// Get{{name}} returns the {{name}} field value +{{#isNullable}} +// If the value is explicit nil, the zero value for {{vendorExtensions.x-go-base-type}} will be returned +{{/isNullable}} +func (o *{{classname}}) Get{{name}}() {{vendorExtensions.x-go-base-type}} { + if o == nil {{#isNullable}}{{^vendorExtensions.x-golang-is-container}}|| o.{{name}}.Get() == nil{{/vendorExtensions.x-golang-is-container}}{{/isNullable}} { + var ret {{vendorExtensions.x-go-base-type}} + return ret + } + +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + return o.{{name}} +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + return *o.{{name}}.Get() +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + return o.{{name}} +{{/isNullable}} +} + +// Get{{name}}Ok returns a tuple with the {{name}} field value +// and a boolean to check if the value has been set. +{{#isNullable}} +// NOTE: If the value is an explicit nil, `nil, true` will be returned +{{/isNullable}} +func (o *{{classname}}) Get{{name}}Ok() ({{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{vendorExtensions.x-go-base-type}}, bool) { + if o == nil {{#isNullable}}{{#vendorExtensions.x-golang-is-container}}|| o.{{name}} == nil{{/vendorExtensions.x-golang-is-container}}{{/isNullable}} { + return nil, false + } +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + return {{^isArray}}{{^isFreeFormObject}}&{{/isFreeFormObject}}{{/isArray}}o.{{name}}, true +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + return o.{{name}}.Get(), o.{{name}}.IsSet() +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + return {{^isArray}}{{^isFreeFormObject}}&{{/isFreeFormObject}}{{/isArray}}o.{{name}}, true +{{/isNullable}} +} + +// Set{{name}} sets field value +func (o *{{classname}}) Set{{name}}(v {{vendorExtensions.x-go-base-type}}) { +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + o.{{name}} = v +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + o.{{name}}.Set(&v) +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + o.{{name}} = v +{{/isNullable}} +} + +{{/required}} +{{^required}} +// Get{{name}} returns the {{name}} field value if set, zero value otherwise{{#isNullable}} (both if not set or set to explicit null){{/isNullable}}. +func (o *{{classname}}) Get{{name}}() {{vendorExtensions.x-go-base-type}} { + if o == nil {{^isNullable}}|| o.{{name}} == nil{{/isNullable}}{{#isNullable}}{{^vendorExtensions.x-golang-is-container}}|| o.{{name}}.Get() == nil{{/vendorExtensions.x-golang-is-container}}{{/isNullable}} { + var ret {{vendorExtensions.x-go-base-type}} + return ret + } +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + return o.{{name}} +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + return *o.{{name}}.Get() +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + return {{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}o.{{name}} +{{/isNullable}} +} + +// Get{{name}}Ok returns a tuple with the {{name}} field value if set, nil otherwise +// and a boolean to check if the value has been set. +{{#isNullable}} +// NOTE: If the value is an explicit nil, `nil, true` will be returned +{{/isNullable}} +func (o *{{classname}}) Get{{name}}Ok() ({{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{vendorExtensions.x-go-base-type}}, bool) { + if o == nil {{^isNullable}}|| o.{{name}} == nil{{/isNullable}}{{#isNullable}}{{#vendorExtensions.x-golang-is-container}}|| o.{{name}} == nil{{/vendorExtensions.x-golang-is-container}}{{/isNullable}} { + return nil, false + } +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + return {{^isArray}}{{^isFreeFormObject}}&{{/isFreeFormObject}}{{/isArray}}o.{{name}}, true +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + return o.{{name}}.Get(), o.{{name}}.IsSet() +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + return o.{{name}}, true +{{/isNullable}} +} + +// Has{{name}} returns a boolean if a field has been set. +func (o *{{classname}}) Has{{name}}() bool { + if o != nil && {{^isNullable}}o.{{name}} != nil{{/isNullable}}{{#isNullable}}{{#vendorExtensions.x-golang-is-container}}o.{{name}} != nil{{/vendorExtensions.x-golang-is-container}}{{^vendorExtensions.x-golang-is-container}}o.{{name}}.IsSet(){{/vendorExtensions.x-golang-is-container}}{{/isNullable}} { + return true + } + + return false +} + +// Set{{name}} gets a reference to the given {{dataType}} and assigns it to the {{name}} field. +func (o *{{classname}}) Set{{name}}(v {{vendorExtensions.x-go-base-type}}) { +{{#isNullable}} +{{#vendorExtensions.x-golang-is-container}} + o.{{name}} = v +{{/vendorExtensions.x-golang-is-container}} +{{^vendorExtensions.x-golang-is-container}} + o.{{name}}.Set({{^isArray}}{{^isFreeFormObject}}&{{/isFreeFormObject}}{{/isArray}}v) +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} +{{^isNullable}} + o.{{name}} = {{^isArray}}{{^isFreeFormObject}}&{{/isFreeFormObject}}{{/isArray}}v +{{/isNullable}} +} +{{#isNullable}} +{{^vendorExtensions.x-golang-is-container}} +// Set{{name}}Nil sets the value for {{name}} to be an explicit nil +func (o *{{classname}}) Set{{name}}Nil() { + o.{{name}}.Set(nil) +} + +// Unset{{name}} ensures that no value is present for {{name}}, not even an explicit nil +func (o *{{classname}}) Unset{{name}}() { + o.{{name}}.Unset() +} +{{/vendorExtensions.x-golang-is-container}} +{{/isNullable}} + +{{/required}} +{{/vars}} +func (o {{classname}}) MarshalJSON() ([]byte, error) { + toSerialize := {{#isArray}}make([]interface{}, len(o.Items)){{/isArray}}{{^isArray}}map[string]interface{}{}{{/isArray}} + {{#parent}} + {{^isMap}} + {{^isArray}} + serialized{{parent}}, err{{parent}} := json.Marshal(o.{{parent}}) + if err{{parent}} != nil { + return []byte{}, err{{parent}} + } + err{{parent}} = json.Unmarshal([]byte(serialized{{parent}}), &toSerialize) + if err{{parent}} != nil { + return []byte{}, err{{parent}} + } + {{/isArray}} + {{/isMap}} + {{#isArray}} + for i, item := range o.Items { + toSerialize[i] = item + } + {{/isArray}} + {{/parent}} + {{#vars}} + {{! if argument is nullable, only serialize it if it is set}} + {{#isNullable}} + {{#vendorExtensions.x-golang-is-container}} + {{! support for container fields is not ideal at this point because of lack of Nullable* types}} + if o.{{name}} != nil { + toSerialize["{{baseName}}"] = o.{{name}} + } + {{/vendorExtensions.x-golang-is-container}} + {{^vendorExtensions.x-golang-is-container}} + if {{#required}}true{{/required}}{{^required}}o.{{name}}.IsSet(){{/required}} { + toSerialize["{{baseName}}"] = o.{{name}}.Get() + } + {{/vendorExtensions.x-golang-is-container}} + {{/isNullable}} + {{! if argument is not nullable, don't set it if it is nil}} + {{^isNullable}} + if {{#required}}true{{/required}}{{^required}}o.{{name}} != nil{{/required}} { + toSerialize["{{baseName}}"] = o.{{name}} + } + {{/isNullable}} + {{/vars}} + {{#isAdditionalPropertiesTrue}} + + for key, value := range o.AdditionalProperties { + toSerialize[key] = value + } + + {{/isAdditionalPropertiesTrue}} + return json.Marshal(toSerialize) +} + +{{#isAdditionalPropertiesTrue}} +func (o *{{{classname}}}) UnmarshalJSON(bytes []byte) (err error) { +{{#parent}} +{{^isMap}} + type {{classname}}WithoutEmbeddedStruct struct { + {{#vars}} + {{^-first}} + {{/-first}} + {{#description}} + // {{{description}}} + {{/description}} + {{name}} {{^required}}{{^isNullable}}*{{/isNullable}}{{/required}}{{{dataType}}} `json:"{{baseName}}{{^required}},omitempty{{/required}}"{{#withXml}} xml:"{{baseName}}{{#isXmlAttribute}},attr{{/isXmlAttribute}}"{{/withXml}}{{#vendorExtensions.x-go-custom-tag}} {{{.}}}{{/vendorExtensions.x-go-custom-tag}}` + {{/vars}} + } + + var{{{classname}}}WithoutEmbeddedStruct := {{{classname}}}WithoutEmbeddedStruct{} + + err = json.Unmarshal(bytes, &var{{{classname}}}WithoutEmbeddedStruct) + if err == nil { + var{{{classname}}} := _{{{classname}}}{} + {{#vars}} + var{{{classname}}}.{{{name}}} = var{{{classname}}}WithoutEmbeddedStruct.{{{name}}} + {{/vars}} + *o = {{{classname}}}(var{{{classname}}}) + } else { + return err + } + + var{{{classname}}} := _{{{classname}}}{} + + err = json.Unmarshal(bytes, &var{{{classname}}}) + if err == nil { + o.{{{parent}}} = var{{{classname}}}.{{{parent}}} + } else { + return err + } + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(bytes, &additionalProperties); err == nil { + {{#vars}} + delete(additionalProperties, "{{{baseName}}}") + {{/vars}} + + // remove fields from embedded structs + reflect{{{parent}}} := reflect.ValueOf(o.{{{parent}}}) + for i := 0; i < reflect{{{parent}}}.Type().NumField(); i++ { + t := reflect{{{parent}}}.Type().Field(i) + + if jsonTag := t.Tag.Get("json"); jsonTag != "" { + fieldName := "" + if commaIdx := strings.Index(jsonTag, ","); commaIdx > 0 { + fieldName = jsonTag[:commaIdx] + } else { + fieldName = jsonTag + } + if fieldName != "AdditionalProperties" { + delete(additionalProperties, fieldName) + } + } + } + + o.AdditionalProperties = additionalProperties + } + + return err +{{/isMap}} +{{#isMap}} + var{{{classname}}} := _{{{classname}}}{} + + if err = json.Unmarshal(bytes, &var{{{classname}}}); err == nil { + *o = {{{classname}}}(var{{{classname}}}) + } + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(bytes, &additionalProperties); err == nil { + {{#vars}} + delete(additionalProperties, "{{{baseName}}}") + {{/vars}} + o.AdditionalProperties = additionalProperties + } + + return err +{{/isMap}} +{{/parent}} +{{^parent}} + var{{{classname}}} := _{{{classname}}}{} + + if err = json.Unmarshal(bytes, &var{{{classname}}}); err == nil { + *o = {{{classname}}}(var{{{classname}}}) + } + + additionalProperties := make(map[string]interface{}) + + if err = json.Unmarshal(bytes, &additionalProperties); err == nil { + {{#vars}} + delete(additionalProperties, "{{{baseName}}}") + {{/vars}} + o.AdditionalProperties = additionalProperties + } + + return err +{{/parent}} +} + +{{/isAdditionalPropertiesTrue}} +{{#isArray}} +func (o *{{{classname}}}) UnmarshalJSON(bytes []byte) (err error) { + return json.Unmarshal(bytes, &o.Items) +} + +{{/isArray}} +{{>nullable_model}} diff --git a/.schema/openapi/templates/go/nullable_model.mustache b/.schema/openapi/templates/go/nullable_model.mustache new file mode 100644 index 0000000..7b60ce6 --- /dev/null +++ b/.schema/openapi/templates/go/nullable_model.mustache @@ -0,0 +1,35 @@ +type Nullable{{{classname}}} struct { + value {{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{{classname}}} + isSet bool +} + +func (v Nullable{{classname}}) Get() {{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{classname}} { + return v.value +} + +func (v *Nullable{{classname}}) Set(val {{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{classname}}) { + v.value = val + v.isSet = true +} + +func (v Nullable{{classname}}) IsSet() bool { + return v.isSet +} + +func (v *Nullable{{classname}}) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullable{{classname}}(val {{^isArray}}{{^isFreeFormObject}}*{{/isFreeFormObject}}{{/isArray}}{{classname}}) *Nullable{{classname}} { + return &Nullable{{classname}}{value: val, isSet: true} +} + +func (v Nullable{{{classname}}}) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *Nullable{{{classname}}}) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/.schema/openapi/templates/go/openapi.mustache b/.schema/openapi/templates/go/openapi.mustache new file mode 100644 index 0000000..51ebafb --- /dev/null +++ b/.schema/openapi/templates/go/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} \ No newline at end of file diff --git a/.schema/openapi/templates/go/partial_header.mustache b/.schema/openapi/templates/go/partial_header.mustache new file mode 100644 index 0000000..ee1ead4 --- /dev/null +++ b/.schema/openapi/templates/go/partial_header.mustache @@ -0,0 +1,18 @@ +/* + {{#appName}} + * {{{appName}}} + * + {{/appName}} + {{#appDescription}} + * {{{appDescription}}} + * + {{/appDescription}} + {{#version}} + * API version: {{{version}}} + {{/version}} + {{#infoEmail}} + * Contact: {{{infoEmail}}} + {{/infoEmail}} + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. diff --git a/.schema/openapi/templates/go/response.mustache b/.schema/openapi/templates/go/response.mustache new file mode 100644 index 0000000..1a8765b --- /dev/null +++ b/.schema/openapi/templates/go/response.mustache @@ -0,0 +1,38 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResonse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/.schema/openapi/templates/go/signing.mustache b/.schema/openapi/templates/go/signing.mustache new file mode 100644 index 0000000..6202dea --- /dev/null +++ b/.schema/openapi/templates/go/signing.mustache @@ -0,0 +1,414 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "net/http" + "net/textproto" + "os" + "strings" + "time" +) + +const ( + // Constants for HTTP signature parameters. + // The '(request-target)' parameter concatenates the lowercased :method, an + // ASCII space, and the :path pseudo-headers. + HttpSignatureParameterRequestTarget string = "(request-target)" + // The '(created)' parameter expresses when the signature was + // created. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterCreated string = "(created)" + // The '(expires)' parameter expresses when the signature ceases to + // be valid. The value MUST be a Unix timestamp integer value. + HttpSignatureParameterExpires string = "(expires)" +) + +const ( + // Constants for HTTP headers. + // The 'Host' header, as defined in RFC 2616, section 14.23. + HttpHeaderHost string = "Host" + // The 'Date' header. + HttpHeaderDate string = "Date" + // The digest header, as defined in RFC 3230, section 4.3.2. + HttpHeaderDigest string = "Digest" + // The HTTP Authorization header, as defined in RFC 7235, section 4.2. + HttpHeaderAuthorization string = "Authorization" +) + +const ( + // Specifies the Digital Signature Algorithm is derived from metadata + // associated with 'keyId'. Supported DSA algorithms are RSASSA-PKCS1-v1_5, + // RSASSA-PSS, and ECDSA. + // The hash is SHA-512. + // This is the default value. + HttpSigningSchemeHs2019 string = "hs2019" + // Use RSASSA-PKCS1-v1_5 with SHA-512 hash. Deprecated. + HttpSigningSchemeRsaSha512 string = "rsa-sha512" + // Use RSASSA-PKCS1-v1_5 with SHA-256 hash. Deprecated. + HttpSigningSchemeRsaSha256 string = "rsa-sha256" + + // RFC 8017 section 7.2 + // Calculate the message signature using RSASSA-PKCS1-V1_5-SIGN from RSA PKCS#1 v1.5. + // PKCSV1_5 is deterministic. The same message and key will produce an identical + // signature value each time. + HttpSigningAlgorithmRsaPKCS1v15 string = "RSASSA-PKCS1-v1_5" + // Calculate the message signature using probabilistic signature scheme RSASSA-PSS. + // PSS is randomized and will produce a different signature value each time. + HttpSigningAlgorithmRsaPSS string = "RSASSA-PSS" +) + +var supportedSigningSchemes = map[string]bool{ + HttpSigningSchemeHs2019: true, + HttpSigningSchemeRsaSha512: true, + HttpSigningSchemeRsaSha256: true, +} + + +// HttpSignatureAuth provides HTTP signature authentication to a request passed +// via context using ContextHttpSignatureAuth. +// An 'Authorization' header is calculated by creating a hash of select headers, +// and optionally the body of the HTTP request, then signing the hash value using +// a private key which is available to the client. +// +// SignedHeaders specifies the list of HTTP headers that are included when generating +// the message signature. +// The two special signature headers '(request-target)' and '(created)' SHOULD be +// included in SignedHeaders. +// The '(created)' header expresses when the signature was created. +// The '(request-target)' header is a concatenation of the lowercased :method, an +// ASCII space, and the :path pseudo-headers. +// +// For example, SignedHeaders can be set to: +// (request-target) (created) date host digest +// +// When SignedHeaders is not specified, the client defaults to a single value, '(created)', +// in the list of HTTP headers. +// When SignedHeaders contains the 'Digest' value, the client performs the following operations: +// 1. Calculate a digest of request body, as specified in RFC3230, section 4.3.2. +// 2. Set the 'Digest' header in the request body. +// 3. Include the 'Digest' header and value in the HTTP signature. +type HttpSignatureAuth struct { + KeyId string // A key identifier. + PrivateKeyPath string // The path to the private key. + Passphrase string // The passphrase to decrypt the private key, if the key is encrypted. + SigningScheme string // The signature scheme, when signing HTTP requests. Supported value is 'hs2019'. + // The signature algorithm, when signing HTTP requests. + // Supported values are RSASSA-PKCS1-v1_5, RSASSA-PSS. + SigningAlgorithm string + SignedHeaders []string // A list of HTTP headers included when generating the signature for the message. + // SignatureMaxValidity specifies the maximum duration of the signature validity. + // The value is used to set the '(expires)' signature parameter in the HTTP request. + // '(expires)' is set to '(created)' plus the value of the SignatureMaxValidity field. + // To specify the '(expires)' signature parameter, set 'SignatureMaxValidity' and add '(expires)' to 'SignedHeaders'. + SignatureMaxValidity time.Duration + privateKey crypto.PrivateKey // The private key used to sign HTTP requests. +} + +// ContextWithValue validates the HttpSignatureAuth configuration parameters and returns a context +// suitable for HTTP signature. An error is returned if the HttpSignatureAuth configuration parameters +// are invalid. +func (h *HttpSignatureAuth) ContextWithValue(ctx context.Context) (context.Context, error) { + if h.KeyId == "" { + return nil, fmt.Errorf("Key ID must be specified") + } + if h.PrivateKeyPath == "" { + return nil, fmt.Errorf("Private key path must be specified") + } + if _, ok := supportedSigningSchemes[h.SigningScheme]; !ok { + return nil, fmt.Errorf("Invalid signing scheme: '%v'", h.SigningScheme) + } + m := make(map[string]bool) + for _, h := range h.SignedHeaders { + if strings.ToLower(h) == strings.ToLower(HttpHeaderAuthorization) { + return nil, fmt.Errorf("Signed headers cannot include the 'Authorization' header") + } + m[h] = true + } + if len(m) != len(h.SignedHeaders) { + return nil, fmt.Errorf("List of signed headers cannot have duplicate names") + } + if h.SignatureMaxValidity < 0 { + return nil, fmt.Errorf("Signature max validity must be a positive value") + } + if err := h.loadPrivateKey(); err != nil { + return nil, err + } + return context.WithValue(ctx, ContextHttpSignatureAuth, *h), nil +} + +// GetPublicKey returns the public key associated with this HTTP signature configuration. +func (h *HttpSignatureAuth) GetPublicKey() (crypto.PublicKey, error) { + if h.privateKey == nil { + if err := h.loadPrivateKey(); err != nil { + return nil, err + } + } + switch key := h.privateKey.(type) { + case *rsa.PrivateKey: + return key.Public(), nil + case *ecdsa.PrivateKey: + return key.Public(), nil + default: + // Do not change '%T' to anything else such as '%v'! + // The value of the private key must not be returned. + return nil, fmt.Errorf("Unsupported key: %T", h.privateKey) + } +} + +// loadPrivateKey reads the private key from the file specified in the HttpSignatureAuth. +func (h *HttpSignatureAuth) loadPrivateKey() (err error) { + var file *os.File + file, err = os.Open(h.PrivateKeyPath) + if err != nil { + return fmt.Errorf("Cannot load private key '%s'. Error: %v", h.PrivateKeyPath, err) + } + defer func() { + err = file.Close() + }() + var priv []byte + priv, err = io.ReadAll(file) + if err != nil { + return err + } + pemBlock, _ := pem.Decode(priv) + if pemBlock == nil { + // No PEM data has been found. + return fmt.Errorf("File '%s' does not contain PEM data", h.PrivateKeyPath) + } + var privKey []byte + if x509.IsEncryptedPEMBlock(pemBlock) { + // The PEM data is encrypted. + privKey, err = x509.DecryptPEMBlock(pemBlock, []byte(h.Passphrase)) + if err != nil { + // Failed to decrypt PEM block. Because of deficiencies in the encrypted-PEM format, + // it's not always possibleto detect an incorrect password. + return err + } + } else { + privKey = pemBlock.Bytes + } + switch pemBlock.Type { + case "RSA PRIVATE KEY": + if h.privateKey, err = x509.ParsePKCS1PrivateKey(privKey); err != nil { + return err + } + case "EC PRIVATE KEY", "PRIVATE KEY": + // https://tools.ietf.org/html/rfc5915 section 4. + if h.privateKey, err = x509.ParsePKCS8PrivateKey(privKey); err != nil { + return err + } + default: + return fmt.Errorf("Key '%s' is not supported", pemBlock.Type) + } + return nil +} + +// SignRequest signs the request using HTTP signature. +// See https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +// +// Do not add, remove or change headers that are included in the SignedHeaders +// after SignRequest has been invoked; this is because the header values are +// included in the signature. Any subsequent alteration will cause a signature +// verification failure. +// If there are multiple instances of the same header field, all +// header field values associated with the header field MUST be +// concatenated, separated by a ASCII comma and an ASCII space +// ', ', and used in the order in which they will appear in the +// transmitted HTTP message. +func SignRequest( + ctx context.Context, + r *http.Request, + auth HttpSignatureAuth) error { + + if auth.privateKey == nil { + return fmt.Errorf("Private key is not set") + } + now := time.Now() + date := now.UTC().Format(http.TimeFormat) + // The 'created' field expresses when the signature was created. + // The value MUST be a Unix timestamp integer value. See 'HTTP signature' section 2.1.4. + created := now.Unix() + + var h crypto.Hash + var err error + var prefix string + var expiresUnix float64 + + if auth.SignatureMaxValidity < 0 { + return fmt.Errorf("Signature validity must be a positive value") + } + if auth.SignatureMaxValidity > 0 { + e := now.Add(auth.SignatureMaxValidity) + expiresUnix = float64(e.Unix()) + float64(e.Nanosecond()) / float64(time.Second) + } + // Determine the cryptographic hash to be used for the signature and the body digest. + switch auth.SigningScheme { + case HttpSigningSchemeRsaSha512, HttpSigningSchemeHs2019: + h = crypto.SHA512 + prefix = "SHA-512=" + case HttpSigningSchemeRsaSha256: + // This is deprecated and should no longer be used. + h = crypto.SHA256 + prefix = "SHA-256=" + default: + return fmt.Errorf("Unsupported signature scheme: %v", auth.SigningScheme) + } + if !h.Available() { + return fmt.Errorf("Hash '%v' is not available", h) + } + + // Build the "(request-target)" signature header. + var sb bytes.Buffer + fmt.Fprintf(&sb, "%s %s", strings.ToLower(r.Method), r.URL.EscapedPath()) + if r.URL.RawQuery != "" { + // The ":path" pseudo-header field includes the path and query parts + // of the target URI (the "path-absolute" production and optionally a + // '?' character followed by the "query" production (see Sections 3.3 + // and 3.4 of [RFC3986] + fmt.Fprintf(&sb, "?%s", r.URL.RawQuery) + } + requestTarget := sb.String() + sb.Reset() + + // Build the string to be signed. + signedHeaders := auth.SignedHeaders + if len(signedHeaders) == 0 { + signedHeaders = []string{HttpSignatureParameterCreated} + } + // Validate the list of signed headers has no duplicates. + m := make(map[string]bool) + for _, h := range signedHeaders { + m[h] = true + } + if len(m) != len(signedHeaders) { + return fmt.Errorf("List of signed headers must not have any duplicates") + } + hasCreatedParameter := false + hasExpiresParameter := false + for i, header := range signedHeaders { + header = strings.ToLower(header) + var value string + switch header { + case strings.ToLower(HttpHeaderAuthorization): + return fmt.Errorf("Cannot include the 'Authorization' header as a signed header.") + case HttpSignatureParameterRequestTarget: + value = requestTarget + case HttpSignatureParameterCreated: + value = fmt.Sprintf("%d", created) + hasCreatedParameter = true + case HttpSignatureParameterExpires: + if auth.SignatureMaxValidity.Nanoseconds() == 0 { + return fmt.Errorf("Cannot set '(expires)' signature parameter. SignatureMaxValidity is not configured.") + } + value = fmt.Sprintf("%.3f", expiresUnix) + hasExpiresParameter = true + case "date": + value = date + r.Header.Set(HttpHeaderDate, date) + case "digest": + // Calculate the digest of the HTTP request body. + // Calculate body digest per RFC 3230 section 4.3.2 + bodyHash := h.New() + if r.Body != nil { + // Make a copy of the body io.Reader so that we can read the body to calculate the hash, + // then one more time when marshaling the request. + var body io.Reader + body, err = r.GetBody() + if err != nil { + return err + } + if _, err = io.Copy(bodyHash, body); err != nil { + return err + } + } + d := bodyHash.Sum(nil) + value = prefix + base64.StdEncoding.EncodeToString(d) + r.Header.Set(HttpHeaderDigest, value) + case "host": + value = r.Host + r.Header.Set(HttpHeaderHost, r.Host) + default: + var ok bool + var v []string + canonicalHeader := textproto.CanonicalMIMEHeaderKey(header) + if v, ok = r.Header[canonicalHeader]; !ok { + // If a header specified in the headers parameter cannot be matched with + // a provided header in the message, the implementation MUST produce an error. + return fmt.Errorf("Header '%s' does not exist in the request", canonicalHeader) + } + // If there are multiple instances of the same header field, all + // header field values associated with the header field MUST be + // concatenated, separated by a ASCII comma and an ASCII space + // `, `, and used in the order in which they will appear in the + // transmitted HTTP message. + value = strings.Join(v, ", ") + } + if i > 0 { + fmt.Fprintf(&sb, "\n") + } + fmt.Fprintf(&sb, "%s: %s", header, value) + } + if expiresUnix != 0 && !hasExpiresParameter { + return fmt.Errorf("SignatureMaxValidity is specified, but '(expired)' parameter is not present") + } + msg := []byte(sb.String()) + msgHash := h.New() + if _, err = msgHash.Write(msg); err != nil { + return err + } + d := msgHash.Sum(nil) + + var signature []byte + switch key := auth.privateKey.(type) { + case *rsa.PrivateKey: + switch auth.SigningAlgorithm { + case HttpSigningAlgorithmRsaPKCS1v15: + signature, err = rsa.SignPKCS1v15(rand.Reader, key, h, d) + case "", HttpSigningAlgorithmRsaPSS: + signature, err = rsa.SignPSS(rand.Reader, key, h, d, nil) + default: + return fmt.Errorf("Unsupported signing algorithm: '%s'", auth.SigningAlgorithm) + } + case *ecdsa.PrivateKey: + signature, err = key.Sign(rand.Reader, d, h) + case ed25519.PrivateKey: // requires go 1.13 + signature, err = key.Sign(rand.Reader, msg, crypto.Hash(0)) + default: + return fmt.Errorf("Unsupported private key") + } + if err != nil { + return err + } + + sb.Reset() + for i, header := range signedHeaders { + if i > 0 { + sb.WriteRune(' ') + } + sb.WriteString(strings.ToLower(header)) + } + headers_list := sb.String() + sb.Reset() + fmt.Fprintf(&sb, `Signature keyId="%s",algorithm="%s",`, auth.KeyId, auth.SigningScheme) + if hasCreatedParameter { + fmt.Fprintf(&sb, "created=%d,", created) + } + if hasExpiresParameter { + fmt.Fprintf(&sb, "expires=%.3f,", expiresUnix) + } + fmt.Fprintf(&sb, `headers="%s",signature="%s"`, headers_list, base64.StdEncoding.EncodeToString(signature)) + authStr := sb.String() + r.Header.Set(HttpHeaderAuthorization, authStr) + return nil +} diff --git a/.schema/openapi/templates/go/utils.mustache b/.schema/openapi/templates/go/utils.mustache new file mode 100644 index 0000000..fed52d7 --- /dev/null +++ b/.schema/openapi/templates/go/utils.mustache @@ -0,0 +1,326 @@ +{{>partial_header}} +package {{packageName}} + +import ( + "encoding/json" + "time" +) + +// PtrBool is a helper routine that returns a pointer to given boolean value. +func PtrBool(v bool) *bool { return &v } + +// PtrInt is a helper routine that returns a pointer to given integer value. +func PtrInt(v int) *int { return &v } + +// PtrInt32 is a helper routine that returns a pointer to given integer value. +func PtrInt32(v int32) *int32 { return &v } + +// PtrInt64 is a helper routine that returns a pointer to given integer value. +func PtrInt64(v int64) *int64 { return &v } + +// PtrFloat32 is a helper routine that returns a pointer to given float value. +func PtrFloat32(v float32) *float32 { return &v } + +// PtrFloat64 is a helper routine that returns a pointer to given float value. +func PtrFloat64(v float64) *float64 { return &v } + +// PtrString is a helper routine that returns a pointer to given string value. +func PtrString(v string) *string { return &v } + +// PtrTime is helper routine that returns a pointer to given Time value. +func PtrTime(v time.Time) *time.Time { return &v } + +type NullableBool struct { + value *bool + isSet bool +} + +func (v NullableBool) Get() *bool { + return v.value +} + +func (v *NullableBool) Set(val *bool) { + v.value = val + v.isSet = true +} + +func (v NullableBool) IsSet() bool { + return v.isSet +} + +func (v *NullableBool) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBool(val *bool) *NullableBool { + return &NullableBool{value: val, isSet: true} +} + +func (v NullableBool) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBool) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableInt struct { + value *int + isSet bool +} + +func (v NullableInt) Get() *int { + return v.value +} + +func (v *NullableInt) Set(val *int) { + v.value = val + v.isSet = true +} + +func (v NullableInt) IsSet() bool { + return v.isSet +} + +func (v *NullableInt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt(val *int) *NullableInt { + return &NullableInt{value: val, isSet: true} +} + +func (v NullableInt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableInt32 struct { + value *int32 + isSet bool +} + +func (v NullableInt32) Get() *int32 { + return v.value +} + +func (v *NullableInt32) Set(val *int32) { + v.value = val + v.isSet = true +} + +func (v NullableInt32) IsSet() bool { + return v.isSet +} + +func (v *NullableInt32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt32(val *int32) *NullableInt32 { + return &NullableInt32{value: val, isSet: true} +} + +func (v NullableInt32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableInt64 struct { + value *int64 + isSet bool +} + +func (v NullableInt64) Get() *int64 { + return v.value +} + +func (v *NullableInt64) Set(val *int64) { + v.value = val + v.isSet = true +} + +func (v NullableInt64) IsSet() bool { + return v.isSet +} + +func (v *NullableInt64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt64(val *int64) *NullableInt64 { + return &NullableInt64{value: val, isSet: true} +} + +func (v NullableInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableFloat32 struct { + value *float32 + isSet bool +} + +func (v NullableFloat32) Get() *float32 { + return v.value +} + +func (v *NullableFloat32) Set(val *float32) { + v.value = val + v.isSet = true +} + +func (v NullableFloat32) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat32(val *float32) *NullableFloat32 { + return &NullableFloat32{value: val, isSet: true} +} + +func (v NullableFloat32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableFloat64 struct { + value *float64 + isSet bool +} + +func (v NullableFloat64) Get() *float64 { + return v.value +} + +func (v *NullableFloat64) Set(val *float64) { + v.value = val + v.isSet = true +} + +func (v NullableFloat64) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat64(val *float64) *NullableFloat64 { + return &NullableFloat64{value: val, isSet: true} +} + +func (v NullableFloat64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableString struct { + value *string + isSet bool +} + +func (v NullableString) Get() *string { + return v.value +} + +func (v *NullableString) Set(val *string) { + v.value = val + v.isSet = true +} + +func (v NullableString) IsSet() bool { + return v.isSet +} + +func (v *NullableString) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableString(val *string) *NullableString { + return &NullableString{value: val, isSet: true} +} + +func (v NullableString) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableString) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + +type NullableTime struct { + value *time.Time + isSet bool +} + +func (v NullableTime) Get() *time.Time { + return v.value +} + +func (v *NullableTime) Set(val *time.Time) { + v.value = val + v.isSet = true +} + +func (v NullableTime) IsSet() bool { + return v.isSet +} + +func (v *NullableTime) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTime(val *time.Time) *NullableTime { + return &NullableTime{value: val, isSet: true} +} + +func (v NullableTime) MarshalJSON() ([]byte, error) { + return v.value.MarshalJSON() +} + +func (v *NullableTime) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/.schema/relation_tuple.schema.json b/.schema/relation_tuple.schema.json new file mode 100644 index 0000000..a8e0c91 --- /dev/null +++ b/.schema/relation_tuple.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": false, + "allOf": [ + { + "required": ["namespace", "relation", "object"], + "properties": { + "$schema": { + "type": "string", + "format": "uri-reference", + "description": "Add this to allow defining the schema, useful for IDE integration" + }, + "namespace": { + "type": "string", + "description": "The namespace of the object and relation in this tuple." + }, + "object": { + "type": "string", + "description": "The object affected by this relation." + }, + "relation": { + "type": "string", + "description": "The relation of the object and subject." + } + } + }, + { + "oneOf": [ + { + "required": ["subject_id"], + "properties": { + "subject_id": { + "type": "string", + "description": "The subject ID affected by this relation." + } + } + }, + { + "required": ["subject_set"], + "properties": { + "subject_set": { + "type": "object", + "description": "The subject set affected by this relation.", + "properties": { + "namespace": { + "type": "string", + "description": "The namespace of the object and relation in this subject set." + }, + "object": { + "type": "string", + "description": "The object referenced in this subject set." + }, + "relation": { + "type": "string", + "description": "The relation of this subject set." + } + }, + "additionalProperties": false, + "required": ["namespace", "relation", "object"] + } + } + } + ] + } + ] +} diff --git a/.schema/version.schema.json b/.schema/version.schema.json new file mode 100644 index 0000000..84072ac --- /dev/null +++ b/.schema/version.schema.json @@ -0,0 +1,216 @@ +{ + "$id": "https://github.com/ory/keto/.schema/versions.config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "oneOf": [ + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.14.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.14.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.12.0-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.12.0-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.11.1-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.11.1-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.11.0-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.11.0-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.10.0-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.10.0-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.9.0-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.9.0-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.0-alpha.2" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.8.0-alpha.2/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.0-alpha.1" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.6.0-alpha.1/internal/driver/config/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.6.0-alpha.3" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.6.0-alpha.3/internal/driver/config/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.0-alpha.0" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.7.0-alpha.0/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.7.0-alpha.1" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.7.0-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "properties": { + "version": { + "const": "v0.8.0-alpha.1" + } + }, + "required": ["version"] + }, + { + "$ref": "https://raw.githubusercontent.com/ory/keto/v0.8.0-alpha.1/.schema/config.schema.json" + } + ] + }, + { + "allOf": [ + { + "oneOf": [ + { + "properties": { + "version": { + "type": "string", + "maxLength": 0 + } + }, + "required": ["version"] + }, + { + "not": { + "properties": { + "version": {} + }, + "required": ["version"] + } + } + ] + }, + { + "$ref": "#/oneOf/0/allOf/1" + } + ] + } + ], + "title": "All Versions of the ORY Keto Configuration", + "type": "object" +} diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..9e2e8bb --- /dev/null +++ b/AUTHORS @@ -0,0 +1,58 @@ +# This is the official list of Ory Keto authors. +# If you don't want to be on this list, please contact Ory. + +aeneasr +Aeneas Rekkas +Andrew Minkin +Anirudh Oppiliappan +Aravind +Arthur Knoepflin +AVA Monitoring <56961522+avamonitoring@users.noreply.github.com> +Benjie Gillam +Canh Dinh +Conor Wyse +csuzhang <48237831+zhyChesterCheung@users.noreply.github.com> +Cypherpunk Samurai <66906402+CypherpunkSamurai@users.noreply.github.com> +Deepak Sharma +Dimas Yudha P +Ferdynand Naczynski +Florian Nagel +fredbi +Guy Zylberberg +Henning Perl +Henry Williams <3443648+henryjw@users.noreply.github.com> +hperl <34397+hperl@users.noreply.github.com> +Ismail Demirbilek +Jakub Błaszczyk +Jan Moxter +jingkai +Joel Hoisko +John +Kevin Goslar +Klaus Herrmann <106238709+kmherrmann@users.noreply.github.com> +Kumbirai Tanekha +Manoel V. Machado +Marc S +Mart Aarma +minchao +Mohammad Aziz +Moritz Heiber +nipsufn +Oscar Gómez +Patrik Neu +Patryk +Paul Crussaire <36532199+DevPolo@users.noreply.github.com> +robbietree8 <73832691+robbietree8@users.noreply.github.com> +Robin Brämer +Rob Liebowitz +Roman Minkin +Satoshi Matsumoto +Shaswat Saxena +Stuart P. Bentley +sumeet jain +Svante Bengtson +Takashi Yoneuchi +Thomas Aidan Curran +Tom Fenech +Trang Mai <40815729+trangmaiq@users.noreply.github.com> +Vincent 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 new file mode 100644 index 0000000..3778127 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,293 @@ + + + +# Contribute to Ory Keto + + + + +- [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) + + + +## Introduction + +_Please note_: We take Ory Keto's security and our users' trust very seriously. +If you believe you have found a security issue in Ory Keto, please disclose it +by contacting us at security@ory.com. + +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. + +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. + +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. + +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 Keto's direction. A great way to do this is via +[Ory Keto Discussions](https://github.com/ory/keto/discussions) or the +[Ory Chat](https://www.ory.com/chat). + +## FAQ + +- I am new to the community. Where can I find the + [Ory Community Code of Conduct?](https://github.com/ory/keto/blob/master/CODE_OF_CONDUCT.md) + +- I have a question. Where can I get + [answers to questions regarding Ory Keto?](#communication) + +- 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) + +- I want to talk to other Ory Keto users. + [How can I become a part of the community?](#communication) + +- I would like to know what I am agreeing to when I contribute to Ory Keto. Does + Ory have + [a Contributors License Agreement?](https://cla-assistant.io/ory/keto) + +- I would like updates about new versions of Ory Keto. + [How are new releases announced?](https://www.ory.com/l/sign-up-newsletter) + +## How can I contribute? + +If you want to start to contribute code right away, take a look at the +[list of good first issues](https://github.com/ory/keto/labels/good%20first%20issue). + +There are many other ways you can contribute. Here are a few things you can do +to help out: + +- **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 Keto. + Github stars help the project gain visibility and stand out. + +- **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). + +- **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. + +- **Help with open issues.** We have a lot of open issues for Ory Keto 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. + +- **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). + +- **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 Keto Discussions](https://github.com/ory/keto/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. + +If you want to receive regular notifications about updates to Ory Keto, consider +joining the mailing list. We will _only_ send you vital information on the +projects that you are interested in. + +Also, [follow us on Twitter](https://twitter.com/orycorp). + +## 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 + +**All code contributions require prior discussion and agreement with maintainers +before opening a pull request.** + +This applies to bug fixes, new features, and refactoring. Before writing code: + +1. Open a [Discussion](https://github.com/ory/keto/discussions/new/choose) or + [Issue](https://github.com/ory/keto/issues/new/choose) describing the problem + and your proposed solution. +2. Wait for maintainer feedback and explicit agreement on the implementation + approach. +3. Only then begin writing code. + +Pull requests without prior discussion may be closed without review. This policy +exists to protect your time and maintainer time. + +"Drive-by" PRs, even well-intentioned ones, often conflict with the project +roadmap, duplicate ongoing work, or introduce architectural inconsistencies. A +quick conversation can help avoid these problems. + +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](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 ./...` (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/keto/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/keto). + +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). + +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/keto); +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. + +### AI-assisted contributions + +AI can be a valuable aid for writing code, documentation, and tests. However, to +maintain the quality and reliability of Ory Keto, please follow these +guidelines: + +- When submitting a pull request or issue that involved AI assistance, mention + the tools you used and the extent of their involvement. This helps reviewers + understand the context of your contribution. + +- Pull requests created with AI assistance should address an existing, accepted + issue. If you have an idea for a new feature or improvement, please open a + discussion or issue first to ensure alignment with the project's direction + before investing time in implementation. + +- You must use AI responsibly when writing code. All contributions must be + tested and verified before submission. Unreviewed AI-generated code will not + be accepted, and repeated submissions of this nature may result in restricted + contribution privileges. + +- When using AI to help draft issues, discussions, or documentation, review and + edit the output before submitting. AI tends to be verbose. Trim unnecessary + content and ensure your submission is clear and focused. + +Contributors must use AI responsibly. These guidelines exist to ensure that +every contribution meets the high standards our community expects, while still +embracing the productivity benefits that AI tools can provide. + +## 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/keto.git + +# Next you add a git remote that is your fork: +git remote add fork git@github.com:/ory/keto.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 +``` + +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. + +[Ory Community Code of Conduct](https://github.com/ory/keto/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](https://www.ory.com/chat). diff --git a/DEVELOP.md b/DEVELOP.md new file mode 100644 index 0000000..2881933 --- /dev/null +++ b/DEVELOP.md @@ -0,0 +1,112 @@ +# Development + +This document explains how to develop Ory Keto, run tests, and work with the +tooling around it. + +## Upgrading and changelog + +New releases might introduce breaking changes. To help you identify and +incorporate those changes, we document these changes in +[UPGRADE.md](./UPGRADE.md) and [CHANGELOG.md](./CHANGELOG.md). + +## Command line documentation + +To see available commands and flags, run: + +```shell +keto -h +# or +keto help +``` + +## Contribution guidelines + +We encourage all contributions. Before opening a pull request, read the +[contribution guidelines](./CONTRIBUTING.md). + +## Prerequisites + +You need Go 1.19+ and, for the test suites: + +- Docker and Docker Compose +- GNU Make 4.3 +- Node.js and npm >= v7 + +It is possible to develop Ory Keto on Windows, but please be aware that all +guides assume a Unix shell like bash or zsh. + +## Install from source + +To install Keto from source: + +```shell +make install +``` + +## Formatting code + +Format all code using: + +```shell +make format +``` + +The continuous integration pipeline checks code formatting. + +## Running tests + +There are two types of tests: + +- Short tests that do not require a SQL database like PostgreSQL +- Regular tests that require PostgreSQL, MySQL, and CockroachDB + +### Short tests + +Short tests run fairly quickly and use SQLite in-memory. + +Run all short tests: + +```shell +go test -short ./... +``` + +Run short tests in a specific module: + +```shell +go test -short ./internal/check/... +``` + +### Regular tests + +Regular tests require a database setup. + +The test suite can work with docker directly using +[ory/dockertest](https://github.com/ory/dockertest), but we encourage using the +script instead. Using dockertest can bloat the number of Docker Images on your +system and starting them on each run is quite slow. + +Run the full test suite: + +```shell +source ./scripts/test-resetdb.sh +go test ./... +``` + +### End-to-end tests + +The e2e tests are part of the normal `go test`. To only run the e2e test, use: + +```shell +source ./scripts/test-resetdb.sh +go test ./internal/e2e/... +``` + +or add the `-short` tag to only test against sqlite in-memory. + +## Build Docker image + +To build a development Docker image: + +```shell +make docker +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + 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 "[]" + 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 + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + 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/Makefile b/Makefile new file mode 100644 index 0000000..ad04663 --- /dev/null +++ b/Makefile @@ -0,0 +1,165 @@ +SHELL=/bin/bash -o pipefail + +export PWD := $(shell pwd) +export PATH := ${PWD}/.bin:${PATH} +export IMAGE_TAG := $(if $(IMAGE_TAG),$(IMAGE_TAG),latest) + +SCRIPT_DEPENDENCIES = grype \ + ory \ + licenses + +define make-script-dependency + # each script is responsible to figure out whether it should re-install + .PHONY: .bin/$1 + .bin/$1: + ./scripts/install-$1.sh +endef +$(foreach dep, $(SCRIPT_DEPENDENCIES), $(eval $(call make-script-dependency,$(dep)))) + +.PHONY: .bin/clidoc +.bin/clidoc: + go build -o .bin/clidoc ./cmd/clidoc/. + +authors: # updates the AUTHORS file + curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/ci/master/authors/authors.sh | env PRODUCT="Ory Keto" bash + +PRETTIER_VERSION=$(shell cat package.json | jq -r '.devDependencies["prettier"] // .dependencies["prettier"]') + +.PHONY: format +format: .bin/ory + .bin/ory dev headers copyright --type=open-source --exclude=.bin --exclude=internal/httpclient --exclude=proto --exclude=oryx + go tool goimports -w -local github.com/ory/keto *.go internal cmd contrib ketoctx ketoapi embedx + @echo "Prettier Version: $(PRETTIER_VERSION)" + npx --package=prettier@$(PRETTIER_VERSION) prettier . --write + +.PHONY: install +install: + go install . + +.PHONY: docker +docker: + DOCKER_BUILDKIT=1 DOCKER_CONTENT_TRUST=1 docker build --progress=plain -t oryd/keto:${IMAGE_TAG} -f .docker/Dockerfile-build . + +# Generates the SDKs +.PHONY: sdk +sdk: .bin/ory node_modules + rm -rf internal/httpclient + go tool swagger generate spec -m -o spec/swagger.json \ + -c github.com/ory/keto \ + -c github.com/ory/x/healthx \ + -x internal/httpclient \ + -x internal/e2e + .bin/ory dev swagger sanitize ./spec/swagger.json + go tool swagger validate ./spec/swagger.json + CIRCLE_PROJECT_USERNAME=ory CIRCLE_PROJECT_REPONAME=keto \ + .bin/ory dev openapi migrate \ + --health-path-tags metadata \ + -p https://raw.githubusercontent.com/ory/x/master/healthx/openapi/patch.yaml \ + -p file://.schema/openapi/patches/meta.yaml \ + spec/swagger.json spec/api.json + + mkdir -p internal/httpclient + + npm run openapi-generator-cli -- generate -i "spec/api.json" \ + -g go \ + -o "internal/httpclient" \ + --git-user-id ory \ + --git-repo-id keto-client-go \ + --git-host github.com \ + --api-name-suffix "Api" \ + --global-property apiTests=false \ + -t .schema/openapi/templates/go \ + -c .schema/openapi/gen.go.yml + + rm internal/httpclient/go.{mod,sum} + + make format + +.PHONY: build +build: + go build + +# +# Generate APIs and client stubs from the definitions +# +.PHONY: buf-gen +buf-gen: node_modules + go tool buf generate proto + # The gen/go module has its own go.mod, so license scanners resolve its + # license at that module root. buf generate does not emit a LICENSE, so copy + # ours in to keep the module covered after every regeneration. + cp LICENSE gen/go/LICENSE + make format + @echo "All code was generated successfully!" + +# +# Lint API definitions +# +.PHONY: buf-lint +buf-lint: + go tool buf lint ./proto + @echo "All lint checks passed successfully!" + +# +# Generate after linting succeeded +# +.PHONY: buf +buf: buf-lint buf-gen + +.PHONY: test-e2e +test-e2e: + go test -failfast -v ./internal/e2e + +.PHONY: test-docs-samples +test-docs-samples: + go tool -n jd # Apparently on the first run the path is the temporary build output and will be deleted again. Later invocations use the correct go build cache path. + PATH=$$PATH:$$(dirname "$$(go tool -n jd)") && \ + (cd ./contrib/docs-code-samples && \ + npm i && \ + npm test) + +.PHONY: fuzz-test +fuzz-test: + go test -fuzz=FuzzParser -fuzztime=10s ./internal/schema + +.PHONY: libfuzzer-fuzz-test +libfuzzer-fuzz-test: .bin/go114-fuzz-build + mkdir -p .fuzzer/fuzz_parser_corpus + .bin/go114-fuzz-build -o ./.fuzzer/parser.a -func LibfuzzerFuzzParser ./internal/schema + clang -fsanitize=fuzzer ./.fuzzer/parser.a -o ./.fuzzer/parser + ./.fuzzer/parser -use_value_profile=1 -timeout=1 ./.fuzzer/fuzz_parser_corpus ./.fuzzer/fuzz_parser_seeds + +.PHONY: libfuzzer-fuzz-test-minimize +libfuzzer-fuzz-test-minimize: .bin/go114-fuzz-build + mkdir -p .fuzzer/fuzz_parser_corpus + mv .fuzzer/fuzz_parser_corpus .fuzzer/fuzz_parser_old_corpus + mkdir -p .fuzzer/fuzz_parser_corpus + .bin/go114-fuzz-build -o ./.fuzzer/parser.a -func LibfuzzerFuzzParser ./internal/schema + clang -fsanitize=fuzzer ./.fuzzer/parser.a -o ./.fuzzer/parser + ./.fuzzer/parser -runs=0 -merge=1 ./.fuzzer/fuzz_parser_corpus ./.fuzzer/fuzz_parser_seeds ./.fuzzer/fuzz_parser_old_corpus + +.PHONY: cve-scan +cve-scan: docker .bin/grype + grype oryd/keto:latest + +.PHONY: pre-release +pre-release: + go tool yq '.services.keto.image = "oryd/keto:'$$DOCKER_TAG'"' -i docker-compose.yml + go tool yq '.services.keto-migrate.image = "oryd/keto:'$$DOCKER_TAG'"' -i docker-compose-mysql.yml + go tool yq '.services.keto-migrate.image = "oryd/keto:'$$DOCKER_TAG'"' -i docker-compose-postgres.yml + +.PHONY: post-release +post-release: + echo "nothing to do" + +.PHONY: generate +generate: .bin/stringer + go generate ./... + make format + +licenses: .bin/licenses node_modules # checks open-source licenses + .bin/licenses + +node_modules: package-lock.json + npm ci + touch node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b3c383 --- /dev/null +++ b/README.md @@ -0,0 +1,617 @@ +

+ Ory Keto - Open Source & Cloud Native Access Control Server +

+ +

+ Chat · + Discussions · + Newsletter · + Docs · + Try Ory Network · + Jobs +

+ +Ory Keto is the first and most popular open source implementation of "Zanzibar: +Google's Consistent, Global Authorization System". It provides a scalable, +performant authorization server for managing permissions at scale. + +--- + + + + +- [What is Ory Keto?](#what-is-ory-keto) + - [Why Ory Keto](#why-ory-keto) +- [Deployment options](#deployment-options) + - [Use Ory Keto on the Ory Network](#use-ory-keto-on-the-ory-network) + - [Self-host Ory Keto](#self-host-ory-keto) +- [Quickstart](#quickstart) +- [Who is using Ory Keto](#who-is-using-ory-keto) +- [Ecosystem](#ecosystem) + - [Ory Kratos: Identity and User Infrastructure and Management](#ory-kratos-identity-and-user-infrastructure-and-management) + - [Ory Hydra: OAuth2 & OpenID Connect Server](#ory-hydra-oauth2--openid-connect-server) + - [Ory Oathkeeper: Identity & Access Proxy](#ory-oathkeeper-identity--access-proxy) + - [Ory Keto: Access Control Policies as a Server](#ory-keto-access-control-policies-as-a-server) +- [Documentation](#documentation) +- [Developing Ory Keto](#developing-ory-keto) +- [Security](#security) + - [Disclosing vulnerabilities](#disclosing-vulnerabilities) +- [Telemetry](#telemetry) + + + +## What is Ory Keto? + +Ory Keto is an open source implementation of "Zanzibar: Google's Consistent, +Global Authorization System". It follows +[cloud architecture best practices](https://www.ory.com/docs/ecosystem/software-architecture-philosophy) +and focuses on: + +- Scalable permission checks based on the Zanzibar model +- The Ory Permission Language for defining access control policies +- Relationship-based access control (ReBAC) +- Low latency permission checks (sub-10ms) +- Horizontal scaling to billions of relationships +- Consistency and high availability + +We recommend starting with the +[Ory Keto introduction docs](https://www.ory.com/docs/keto) to learn more about +its architecture, feature set, and how it compares to other systems. + +### Why Ory Keto + +Ory Keto is designed to: + +- Implement Google's Zanzibar authorization model at scale +- Provide low-latency permission checks for billions of relationships +- Support the Ory Permission Language for flexible access control +- Work with any identity provider through integration points +- Scale horizontally without effort +- Fit into modern cloud native environments such as Kubernetes and managed + platforms + +## Deployment options + +You can run Ory Keto in two main ways: + +- As a managed service on the Ory Network +- As a self hosted service under your own control, with or without the Ory + Enterprise License + +### Use Ory Keto on the Ory Network + +The [Ory Network](https://www.ory.com/cloud) is the fastest way to use Ory +services in production. **Ory Permissions** is powered by the open source Ory +Keto server and is API compatible. + +The Ory Network provides: + +- Low latency permission checks based on Google's Zanzibar model with built-in + support for the Ory Permission Language +- Identity and credential management that scales to billions of users and + devices +- Registration, login, and account management flows for passkeys, biometrics, + social login, SSO, and multi factor authentication +- OAuth2 and OpenID Connect for single sign on, API access, and machine to + machine authorization +- GDPR friendly storage with data locality and compliance in mind +- Web based Ory Console and Ory CLI for administration and operations +- Cloud native APIs compatible with the open source servers +- Fair, usage based [pricing](https://www.ory.com/pricing) + +Sign up for a +[free developer account](https://console.ory.sh/registration?utm_source=github&utm_medium=banner&utm_campaign=keto-readme) +to get started. + +### Self-host Ory Keto + +You can run Ory Keto yourself for full control over infrastructure, deployment, +and customization. + +The [install guide](https://www.ory.com/docs/keto/install) explains how to: + +- Install Keto on Linux, macOS, Windows, and Docker +- Configure databases such as PostgreSQL, MySQL, and CockroachDB +- Deploy to Kubernetes and other orchestration systems +- Build Keto from source + +This guide uses the open source distribution to get you started without license +requirements. It is a great fit for individuals, researchers, hackers, and +companies that want to experiment, prototype, or run unimportant workloads +without SLAs. You get the full core engine, and you are free to inspect, extend, +and build it from source. + +If you run Keto as part of a business-critical system, you should use a +commercial agreement to reduce operational and security risk. The **Ory +Enterprise License (OEL)** layers on top of self-hosted Keto and provides: + +- Additional enterprise features that are not available in the open source + version +- Regular security releases, including CVE patches, with service level + agreements +- Support for advanced scaling, multi-tenancy, and complex deployments +- Premium support options with SLAs, direct access to engineers, and onboarding + help +- Access to a private Docker registry with frequent and vetted, up-to-date + enterprise builds + +For guaranteed CVE fixes, current enterprise builds, advanced features, and +support in production, you need a valid +[Ory Enterprise License](https://www.ory.com/ory-enterprise-license) and access +to the Ory Enterprise Docker registry. To learn more, +[contact the Ory team](https://www.ory.com/contact/). + +## Quickstart + +Install the [Ory CLI](https://www.ory.com/docs/guides/cli/installation) and +create a new project to try Ory Permissions. + +```bash +# Install the Ory CLI if you do not have it yet: +bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b . ory +sudo mv ./ory /usr/local/bin/ + +# Sign in or sign up +ory auth + +# Create a new project +ory create project --create-workspace "Ory Open Source" --name "GitHub Quickstart" --use-project +``` + +Create a namespace with the Ory Permission Language: + +```bash +# Write a simple configuration with one namespace +echo "class Document implements Namespace {}" > config.ts + +# Apply that configuration +ory patch opl -f file://./config.ts + +# Create a relationship that grants tom access to a document +echo "Document:secret#read@tom" \ + | ory parse relation-tuples --format=json - \ + | ory create relation-tuples - + +# List all relationships +ory list relation-tuples + +# Check if tom can read the document +ory check permission Document:secret read tom +``` + +## Who is using Ory Keto + + + +The Ory community stands on the shoulders of individuals, companies, and +maintainers. The Ory team thanks everyone involved - from submitting bug reports +and feature requests, to contributing patches and documentation. The Ory +community counts more than 50.000 members and is growing. The Ory stack protects +7.000.000.000+ API requests every day across thousands of companies. None of +this would have been possible without each and everyone of you! + +The following list represents companies that have accompanied us along the way +and that have made outstanding contributions to our ecosystem. _If you think +that your company deserves a spot here, reach out to +office@ory.com now_! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameLogoWebsiteCase Study
OpenAI + + + OpenAI + + openai.comOpenAI Case Study
Fandom + + + Fandom + + fandom.comFandom Case Study
Lumin + + + Lumin + + luminpdf.comLumin Case Study
Sencrop + + + Sencrop + + sencrop.comSencrop Case Study
OSINT Industries + + + OSINT Industries + + osint.industriesOSINT Industries Case Study
HGV + + + HGV + + hgv.itHGV Case Study
Maxroll + + + Maxroll + + maxroll.ggMaxroll Case Study
Zezam + + + Zezam + + zezam.ioZezam Case Study
T.RowePrice + + + T.RowePrice + + troweprice.com
Mistral + + + Mistral + + mistral.ai
Axel Springer + + + Axel Springer + + axelspringer.com
Hemnet + + + Hemnet + + hemnet.se
Cisco + + + Cisco + + cisco.com
Presidencia de la República Dominicana + + + Presidencia de la República Dominicana + + presidencia.gob.do
Moonpig + + + Moonpig + + moonpig.com
Booster + + + Booster + + choosebooster.com
Zaptec + + + Zaptec + + zaptec.com
Klarna + + + Klarna + + klarna.com
Raspberry PI Foundation + + + Raspberry PI Foundation + + raspberrypi.org
Tulip + + + Tulip Retail + + tulip.com
Hootsuite + + + Hootsuite + + hootsuite.com
Segment + + + Segment + + segment.com
Arduino + + + Arduino + + arduino.cc
Sainsbury's + + + Sainsbury's + + sainsburys.co.uk
Contraste + + + Contraste + + contraste.com
inMusic + + + InMusic + + inmusicbrands.com
Buhta + + + Buhta + + buhta.com
Amplitude + + + amplitude.com + + amplitude.com
TIER IVKyma ProjectSerloPadis
CloudbearSecurity Onion SolutionsFactlyAll My Funds
NortalOrderMyGearR2DevopsParalus
dyrector.iopinniped.devpvotal.tech
+ +Many thanks to all individual contributors + + + + + +## Ecosystem + + + +We build Ory on several guiding principles when it comes to our architecture +design: + +- Minimal dependencies +- Runs everywhere +- Scales without effort +- Minimize room for human and network errors + +Ory's architecture is designed to run best on a Container Orchestration system +such as Kubernetes, CloudFoundry, OpenShift, and similar projects. Binaries are +small (5-15MB) and available for all popular processor types (ARM, AMD64, i386) +and operating systems (FreeBSD, Linux, macOS, Windows) without system +dependencies (Java, Node, Ruby, libxml, ...). + +### Ory Kratos: Identity and User Infrastructure and Management + +[Ory Kratos](https://github.com/ory/kratos) is an API-first Identity and User +Management system that is built according to +[cloud architecture best practices](https://www.ory.com/docs/next/ecosystem/software-architecture-philosophy). +It implements core use cases that almost every software application needs to +deal with: Self-service Login and Registration, Multi-Factor Authentication +(MFA/2FA), Account Recovery and Verification, Profile, and Account Management. + +### Ory Hydra: OAuth2 & OpenID Connect Server + +[Ory Hydra](https://github.com/ory/hydra) is an OpenID Certified™ OAuth2 and +OpenID Connect Provider which easily connects to any existing identity system by +writing a tiny "bridge" application. It gives absolute control over the user +interface and user experience flows. + +### Ory Oathkeeper: Identity & Access Proxy + +[Ory Oathkeeper](https://github.com/ory/oathkeeper) is a BeyondCorp/Zero Trust +Identity & Access Proxy (IAP) with configurable authentication, authorization, +and request mutation rules for your web services: Authenticate JWT, Access +Tokens, API Keys, mTLS; Check if the contained subject is allowed to perform the +request; Encode resulting content into custom headers (`X-User-ID`), JSON Web +Tokens and more! + +### Ory Keto: Access Control Policies as a Server + +[Ory Keto](https://github.com/ory/keto) is a policy decision point. It uses a +set of access control policies, similar to AWS IAM Policies, in order to +determine whether a subject (user, application, service, car, ...) is authorized +to perform a certain action on a resource. + + + +## Documentation + +The full Ory Keto documentation is available at +[www.ory.com/docs/keto](https://www.ory.com/docs/keto), including: + +- [Installation guides](https://www.ory.com/docs/keto/install) +- [Configuration reference](https://www.ory.com/docs/keto/reference/configuration) +- [HTTP API documentation](https://www.ory.com/docs/keto/sdk/api) +- [Ory Permission Language guide](https://www.ory.com/docs/keto/modeling/create-permission-model) + +For upgrading and changelogs, check [UPGRADE.md](./UPGRADE.md) and +[CHANGELOG.md](./CHANGELOG.md). + +## Developing Ory Keto + +See [DEVELOP.md](./DEVELOP.md) for information on: + +- Contribution guidelines +- Prerequisites +- Install from source +- Running tests +- Build Docker image + +## Security + +### Disclosing vulnerabilities + +If you think you found a security vulnerability, please refrain from posting it +publicly on the forums, the chat, or GitHub. You can find all info for +responsible disclosure in our +[security.txt](https://www.ory.com/.well-known/security.txt). + +## Telemetry + +Our services collect summarized, anonymized data that can optionally be turned +off. Click [here](https://www.ory.com/docs/ecosystem/sqa) to learn more. 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/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..12bef09 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,222 @@ +# Upgrading + +The intent of this document is to make migration of breaking changes as easy as +possible. Please note that not all breaking changes might be included here. +Please check the [CHANGELOG.md](./CHANGELOG.md) for a full list of changes +before finalizing the upgrade process. + + + + +- [0.4.0](#040) +- [0.3.0-sandbox](#030-sandbox) + - [Configuration](#configuration) + - [ORY Access Control Policies Allowed Endpoint](#ory-access-control-policies-allowed-endpoint) + - [SDK](#sdk) +- [0.2.0-sandbox](#020-sandbox) + - [Conceptual changes](#conceptual-changes) + - [Deprecated](#deprecated) + - [Changes](#changes) + - [Additions](#additions) + - [Untouched](#untouched) + - [API Changes](#api-changes) + - [Renamed Endpoints](#renamed-endpoints) + - [Reworked Endpoints](#reworked-endpoints) + - [New Endpoints](#new-endpoints) + - [Migration](#migration) + - [SQL](#sql) +- [0.0.1](#001) + - [CORS is disabled by default](#cors-is-disabled-by-default) + + + +## 0.4.0-sandbox + +This release focuses on a rework of the SDK pipeline. First of all, we have +introduced new SDKs for all popular programming languages and published them on +their respective package repositories: + +- [Python](https://pypi.org/project/ory-keto-client/) +- [PHP](https://packagist.org/packages/ory/keto-client) +- [Go](https://github.com/ory/keto-client-go) +- [NodeJS](https://www.npmjs.com/package/@oryd/keto-client) (with TypeScript) +- [Java](https://search.maven.org/artifact/sh.ory.keto/keto-client) +- [Ruby](https://rubygems.org/gems/ory-keto-client) + +The SDKs hosted in this repository (under ./sdk/...) have been completely +removed. Please use only the SDKs from the above sources from now on as it will +also remove several issues that were caused by the previous SDK pipeline. + +Unfortunately, there were breaking changes introduced by the new SDK generation: + +- Several structs and fields have been renamed in the Go SDK. However, nothing + else changed so upgrading should be a matter of half an hour if you made + extensive use of the SDK, or several minutes if just one or two methods are + being used. +- All other SDKs changed to `openapi-generator`, which is a better maintained + generator that creates better code than the one previously used. This + manifests in TypeScript definitions for the NodeJS SDK and several other + goodies. We do not have a proper migration path for those, unfortunately. + +If you have issues with upgrading the SDK, please let us know in an issue on +this repository! + +## 0.3.0-sandbox + +### Configuration + +The configuration management was updated and now allows configuration via a +config file. Environment variables can still be used to configure ORY Keto but +have been updated. However, old env vars still work but will yield a warning. + +An overview of an exemplary configuration file can be found in +[./docs/config.yml](https://github.com/ory/hydra/blob/master/docs/config.yaml). + +### ORY Access Control Policies Allowed Endpoint + +Endpoint `/engines/acp/ory/{flavor}/allowed` now returns a 403 error when the +request is disallowed. + +### SDK + +Generation of the Go SDK has moved from +[`swagger-codegen`](https://github.com/swagger-api/swagger-codegen) to +[`go-swagger`](https://github.com/go-swagger/go-swagger). If you wish to migrate +your existing SDK integration please open an issue. + +## 0.2.0-sandbox + +ORY Keto has been completely reworked. The major goals of this refactoring are: + +1. To allow easy extension of existing access control mechanisms. +2. Improve stability and responsiveness. +3. Support more than one access control mechanism. Future mechanisms include: + RBAC, ACL, AWS IAM Policies, ... + +We know that these changes seem massive. They are, but they will benefit the +long-term use of this particular piece of software, and they will allow you to +build better systems. + +If you relied on ORY Keto before this release and you are looking for a +migration path, don't hesitate to ask in +[Ory Community Chat](https://slack.ory.com/) or open a +[GitHub issue](https://github.com/ory/keto/issues/new/). Feel free to do the +same if you want the access control policy feature implemented in ORY Hydra +before version `1.0.0`. + +### Conceptual changes + +#### Deprecated + +The following things have been completely deprecated: + +1. Authorizers, +2. Previous storage mechanisms. + +#### Changes + +The following things have changed: + +1. ORY Keto no longer uses ORY Ladon as the engine but instead relies on the + [Open Policy Agent](http://openpolicyagent.org/). The concept of ORY Ladon + Access Policies are working exactly like before, the internal logic however + was rewritten in Rego. +2. The "Warden" concept has been deprecated and replaced. +3. The CLI commands have changed - apart from `serve`, `version`, + `migrate sql` - entirely. +4. The API has changed (read the next section for information on this). +5. Environment variables changed or have been removed. + +#### Additions + +The following things have been added: + +1. ORY (Ladon) Access Control Policies with `exact` string `matching-strategy`. +2. ORY (Ladon) Access Control Policies with `glob` string `matching-strategy`. + +#### Untouched + +The following things remain conceptually untouched: + +1. ORY (Ladon) Access Control Policies with `regex` string `matching-strategy`. + This is the logic that ORY Ladon and previous versions of ORY Keto implement. + +### API Changes + +#### Renamed Endpoints + +- `GET,PUT,POST,DELETE /policies[/]` moved to + `/engines/acp/ory//policies[/]`. + - `POST /policies` has been deprecated and merged with `PUT /policies/` + which is now available at + `PUT /engines/acp/ory//policies` and will upsert (insert + or update) the policy identified by the `id` field in the JSON payload. + - The request & response payloads **did not change** nor did any of the + concepts. +- `GET,PUT,POST,DELETE /roles[/]` moved to + `/engines/acp/ory//roles[/]`. + - `POST /roles` has been deprecated and merged with `PUT /roles/` which is + now available at `PUT /engines/acp/ory//policies` and + will upsert (insert or update) the role identified by the `id` field in the + JSON payload. + - The request & response payloads **did not change** nor did any of the + concepts. +- `POST,GET /roles//members` move to + `/engines/acp/ory//roles//members`. + - `POST /roles` has been moved to + `PUT /engines/acp/ory//policies//members` and will + upsert (insert or update) the role identified by the `id` field in the URL + path. + - The request & response payloads **did not change** nor did any of the + concepts. + +#### Reworked Endpoints + +The Warden concept has been deprecated. Previously, it was possible to send +credentials alongside requests for prior authentication. This concept interfered +with the clear boundary ORY Keto is focusing on, which is permissioning +concepts. + +The Warden API featured endpoints such as: + +- `/warden/oauth2/access-tokens/authorize`: Permformed OAuth 2.0 Token + Introspection on the `token` field, took the `sub` value of the introspection + and used that as input to ORY (Ladon) Access Control Policies. +- `/warden/oauth2/clients/authorize`: Validated the HTTP Basic Authorization + Header using the OAuth 2.0 Client Credentials grant and took the `username` + value of the HTTP Basic Authorization Header and used that as input to ORY + (Ladon) Access Control Policies. + +These endpoints have been deprecated without replacement. Another endpoint was +`/warden/subjects/authorize` which used the format +`{ "subject": "peter", "action": "delete", "resource": "something:valuable" }` +as syntax. This endpoint is available in the exact same format at +`/engines/acp/ory//allowed`. + +#### New Endpoints + +- `GET /version`: Returns the running software version. +- `GET /health/ready`: Returns `{"status": "ok"}` with a 200 HTTP response if + the service is ready to accept connections and handle data. +- `GET /health/alive`: Returns `{"status": "ok"}` with a 200 HTTP response if + the service is ready to accept connections. + +### Migration + +If you relied on ORY Keto before this release and you are looking for a +migration path, don't hesitate to [contact us](mailto:hi@ory.com). We will help +you migrate and improve this guide as we see more migration use cases. + +#### SQL + +The SQL schema changed completely and it is not possible to migrate from the +previous version to this version with just using `keto migrate sql`. Please ask +in [the forums](https://community.ory.sh/) or open a +[GitHub issue](https://github.com/ory/keto/issues/new/) if this affects you. + +## 0.0.1 + +### CORS is disabled by default + +A new environment variable `CORS_ENABLED` was introduced. It sets whether CORS +is enabled ("true") or not ("false")". Default is disabled. diff --git a/benchtest.new.txt b/benchtest.new.txt new file mode 100644 index 0000000..ce12ba9 --- /dev/null +++ b/benchtest.new.txt @@ -0,0 +1,7 @@ +goos: linux +goarch: amd64 +pkg: github.com/ory/keto/internal/check +cpu: 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz +BenchmarkComputedUsersets/Computed_userset-8 14576 81280 ns/op 1.000 queries/op 17675 B/op 364 allocs/op +PASS +ok github.com/ory/keto/internal/check 2.055s diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..ea9876c --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,48 @@ +version: v2 + +inputs: + - module: buf.build/ory/keto + +plugins: + - local: ["go", "tool", "protoc-gen-go"] + out: gen/go + opt: paths=source_relative + + - local: ["go", "tool", "protoc-gen-go-grpc"] + out: gen/go + opt: paths=source_relative + + - local: ["go", "tool", "protoc-gen-connect-go"] + out: gen/go + opt: paths=source_relative + + - local: ["npx", "@bufbuild/protoc-gen-es"] + out: gen/es + + - local: ["npx", "@connectrpc/protoc-gen-connect-es"] + out: gen/es + + - local: ["go", "tool", "protoc-gen-doc"] + out: gen/doc + opt: proto/markdown.tmpl,gen/doc/buf.md + strategy: all + +managed: + enabled: true + # currently not used but keep it for future reference, as it took me quite some time to figure out + # disable: + # - file_option: go_package + # module: buf.build/googleapis/googleapis + override: + - file_option: go_package_prefix + value: github.com/ory/keto/gen/go + - file_option: go_package + value: github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples + path: ory/keto/relation_tuples/v1alpha2 + - file_option: go_package + value: github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1;opl + path: ory/keto/opl/v1alpha1 + # currently not used but keep it for future reference, as it took me quite some time to figure out + # - file_option: go_package_prefix + # module: buf.build/grpc-ecosystem/grpc-gateway + # value: github.com/grpc-ecosystem/grpc-gateway/v2 diff --git a/buf.lock b/buf.lock new file mode 100644 index 0000000..1f7d077 --- /dev/null +++ b/buf.lock @@ -0,0 +1,12 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 80ab13bee0bf4272b6161a72bf7034e0 + digest: b5:1aa6a965be5d02d64e1d81954fa2e78ef9d1e33a0c30f92bc2626039006a94deb3a5b05f14ed8893f5c3ffce444ac008f7e968188ad225c4c29c813aa5f2daa1 + - name: buf.build/googleapis/googleapis + commit: 004180b77378443887d3b55cabc00384 + digest: b5:e8f475fe3330f31f5fd86ac689093bcd274e19611a09db91f41d637cb9197881ce89882b94d13a58738e53c91c6e4bae7dc1feba85f590164c975a89e25115dc + - name: buf.build/grpc-ecosystem/grpc-gateway + commit: 6467306b4f624747aaf6266762ee7a1c + digest: b5:c2caa61467d992749812c909f93c07e9a667da33c758a7c1973d63136c23b3cafcc079985b12cdf54a10049ed3297418f1eda42cdffdcf34113792dcc3a990af diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..1d43d69 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,27 @@ +version: v2 +modules: + - path: proto + name: buf.build/ory/keto + excludes: + - proto/node_modules +deps: + - buf.build/bufbuild/protovalidate + - buf.build/googleapis/googleapis + - buf.build/grpc-ecosystem/grpc-gateway +lint: + use: + - STANDARD + except: + - ENUM_NO_ALLOW_ALIAS + - ENUM_VALUE_PREFIX + - ENUM_VALUE_UPPER_SNAKE_CASE + - ENUM_ZERO_VALUE_SUFFIX + - FIELD_NOT_REQUIRED + - PACKAGE_NO_IMPORT_CYCLE + disallow_comment_ignores: true +breaking: + use: + - FILE + except: + - EXTENSION_NO_DELETE + - FIELD_SAME_DEFAULT diff --git a/cmd/check/root.go b/cmd/check/root.go new file mode 100644 index 0000000..95c8d05 --- /dev/null +++ b/cmd/check/root.go @@ -0,0 +1,92 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/cmd/helpers" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +type checkOutput struct { + Allowed bool `json:"allowed"` +} + +func (o *checkOutput) String() string { + if o.Allowed { + return "Allowed\n" + } + return "Denied\n" +} + +const FlagMaxDepth = "max-depth" + +func NewCheckCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "check : :", + Short: "Check whether a subject has a relation on an object", + Long: "Check whether a subject has a relation on an object.\n\n" + + "Example:\n" + + " keto check User:Alice view Doc:readme\n\n" + + "Legacy format 'keto check : ' is deprecated.", + Args: cobra.RangeArgs(3, 4), + RunE: check, + } + + client.RegisterRemoteURLFlags(cmd.Flags()) + cmdx.RegisterFormatFlags(cmd.Flags()) + cmd.Flags().Int32P(FlagMaxDepth, "d", 0, "Maximum depth of the search tree. If the value is less than 1 or greater than the global max-depth then the global max-depth will be used instead.") + + return cmd +} + +func check(cmd *cobra.Command, args []string) error { + namespace, object, err := helpers.ParseNamespaceObject(cmd, args[2:]) + if err != nil { + return err + } + + sub, err := helpers.ParseSubject(args[0]) + if err != nil { + return fmt.Errorf("could not parse subject %q: %w", args[0], err) + } + conn, err := client.GetReadConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + maxDepth, err := cmd.Flags().GetInt32(FlagMaxDepth) + if err != nil { + return err + } + + cl := rts.NewCheckServiceClient(conn) + resp, err := cl.Check(cmd.Context(), &rts.CheckRequest{ + Tuple: &rts.RelationTuple{ + Namespace: namespace, + Object: object, + Relation: args[1], + Subject: sub, + }, + MaxDepth: maxDepth, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + cmdx.PrintJSONAble(cmd, &checkOutput{Allowed: resp.Allowed}) + return nil +} + +func RegisterCommandsRecursive(parent *cobra.Command) { + parent.AddCommand(NewCheckCmd()) +} diff --git a/cmd/check/root_test.go b/cmd/check/root_test.go new file mode 100644 index 0000000..01d6704 --- /dev/null +++ b/cmd/check/root_test.go @@ -0,0 +1,92 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/client/clienttest" + "github.com/ory/keto/cmd/relationtuple" + "github.com/ory/keto/internal/namespace" +) + +func TestCheckCommand(t *testing.T) { + nspace := &namespace.Namespace{Name: t.Name()} + nspaceUser := &namespace.Namespace{Name: "User"} + + newCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + } + RegisterCommandsRecursive(cmd) + relationtuple.RegisterCommandsRecursive(cmd) + return cmd + } + + ts := clienttest.NewTestServer(t, []*namespace.Namespace{nspace, nspaceUser}, newCmd) + + tuple1 := clienttest.RandomTupleWithSubjectID(nspace.Name) + tuple2 := clienttest.RandomTupleWithSubjectSet(nspace.Name, nspaceUser.Name) + ts.Cmd.ExecNoErr(t, "relation-tuple", "create", tuple2.SubjectSet.String(), tuple2.Relation, tuple2.Namespace+":"+tuple2.Object) + ts.Cmd.ExecNoErr(t, "relation-tuple", "create", *tuple1.SubjectID, tuple1.Relation, tuple1.Namespace+":"+tuple1.Object) + + t.Run("case=SubjectSet", func(t *testing.T) { + subject := tuple2.SubjectSet.String() + rel := tuple2.Relation + nsObj := tuple2.Namespace + ":" + tuple2.Object + + noPermSubject := tuple2.SubjectSet.Namespace + ":no-perm-subject" + + t.Run("case=allowed when tuple exists", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "check", subject, rel, nsObj) + require.Equal(t, "Allowed\n", stdOut) + }) + + t.Run("case=denied for unrelated subject", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "check", noPermSubject, rel, nsObj) + require.Equal(t, "Denied\n", stdOut) + }) + + t.Run("case=4-arg format still works with deprecation warning", func(t *testing.T) { + stdOut, stdErr, err := ts.Cmd.Exec(nil, "check", subject, rel, tuple2.Namespace, tuple2.Object) + require.NoError(t, err) + require.Equal(t, "Allowed\n", stdOut) + require.Contains(t, stdErr, "deprecated") + }) + + t.Run("case=errors on invalid namespace:object format", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "check", subject, rel, "no-colon-here") + require.Error(t, err) + require.Contains(t, stdErr, "expected : format") + }) + }) + + t.Run("case=SubjectID", func(t *testing.T) { + subject := *tuple1.SubjectID + rel := tuple1.Relation + nsObj := tuple1.Namespace + ":" + tuple1.Object + + noPermSubject := "no-perm-subject" + + t.Run("case=SubjectID, allowed when tuple exists", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "check", subject, rel, nsObj) + require.Equal(t, "Allowed\n", stdOut) + }) + + t.Run("case=denied for unrelated object", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "check", noPermSubject, rel, nspace.Name+":nope") + require.Equal(t, "Denied\n", stdOut) + }) + + t.Run("case=4-arg format still works with deprecation warning", func(t *testing.T) { + stdOut, stdErr, err := ts.Cmd.Exec(nil, "check", subject, rel, tuple1.Namespace, tuple1.Object) + require.NoError(t, err) + require.Equal(t, "Allowed\n", stdOut) + require.Contains(t, stdErr, "deprecated") + }) + }) +} diff --git a/cmd/clidoc/main.go b/cmd/clidoc/main.go new file mode 100644 index 0000000..58f881a --- /dev/null +++ b/cmd/clidoc/main.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "os" + + "github.com/ory/keto/cmd" + + "github.com/ory/x/clidoc" +) + +func main() { + if err := clidoc.Generate(cmd.NewRootCmd(), os.Args[1:]); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%+v", err) + os.Exit(1) + } + fmt.Println("All files have been generated and updated.") +} diff --git a/cmd/client/clienttest/fixtures.go b/cmd/client/clienttest/fixtures.go new file mode 100644 index 0000000..d23a9ac --- /dev/null +++ b/cmd/client/clienttest/fixtures.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package clienttest + +import ( + "github.com/ory/x/randx" + + "github.com/ory/keto/ketoapi" +) + +func RandomTupleWithSubjectSet(ns1, ns2 string) *ketoapi.RelationTuple { + return &ketoapi.RelationTuple{ + Namespace: ns1, + Object: randx.MustString(8, randx.AlphaNum), + Relation: randx.MustString(8, randx.AlphaNum), + SubjectSet: &ketoapi.SubjectSet{ + Namespace: ns2, + Object: randx.MustString(8, randx.AlphaNum), + }, + } +} + +func RandomTupleWithSubjectID(ns1 string) *ketoapi.RelationTuple { + return &ketoapi.RelationTuple{ + Namespace: ns1, + Object: randx.MustString(8, randx.AlphaNum), + Relation: randx.MustString(8, randx.AlphaNum), + SubjectID: new(randx.MustString(8, randx.AlphaNum)), + } +} diff --git a/cmd/client/clienttest/test_helpers.go b/cmd/client/clienttest/test_helpers.go new file mode 100644 index 0000000..9bda030 --- /dev/null +++ b/cmd/client/clienttest/test_helpers.go @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package clienttest provides an in-process Keto server for testing the CLI +// commands. It lives outside of package client because it depends on the whole +// Keto server: anything importing package client would otherwise link the +// server, the test framework, and their dependencies into its binary. +package clienttest + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/ory/x/cmdx" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" +) + +type ( + TestServer struct { + Reg driver.Registry + Cmd *cmdx.CommandExecuter + } + ServerType string +) + +const ( + WriteServer ServerType = "write" + ReadServer ServerType = "read" + OplServer ServerType = "opl" +) + +func (st ServerType) FlagName() string { + switch st { + case WriteServer: + return client.FlagWriteRemote + case ReadServer: + return client.FlagReadRemote + case OplServer: + return client.FlagOplRemote + default: + panic(fmt.Sprintf("unknown ServerType %s", st)) + } +} + +func NewTestServer(t *testing.T, nspaces []*namespace.Namespace, newCmd func() *cobra.Command, registryOpts ...driver.TestRegistryOption) *TestServer { + reg := driver.NewSqliteTestRegistry(t, append(registryOpts, driver.WithSelfSignedTransportCredentials())...) + + require.NoError(t, reg.Config(t.Context()).Set(config.KeyNamespaces, nspaces)) + + ts := &TestServer{ + Reg: reg, + Cmd: &cmdx.CommandExecuter{ + New: newCmd, + PersistentArgs: []string{"--insecure-skip-hostname-verification=true"}, + Ctx: t.Context(), + }, + } + ts.serve(t) + return ts +} + +func (ts *TestServer) serve(t *testing.T, serverTypes ...ServerType) { + if len(serverTypes) == 0 { + serverTypes = []ServerType{WriteServer, ReadServer} + } + + for _, st := range serverTypes { + s := httptest.NewUnstartedServer(ts.NewHandler(t, st)) + s.EnableHTTP2 = true + s.StartTLS() + t.Cleanup(s.Close) + + ts.Cmd.PersistentArgs = append(ts.Cmd.PersistentArgs, "--"+st.FlagName(), s.Listener.Addr().String()) + } +} + +func (ts *TestServer) NewHandler(t *testing.T, st ServerType) http.Handler { + switch st { + case ReadServer: + return ts.Reg.ReadRouter(t.Context()) + case WriteServer: + return ts.Reg.WriteRouter(t.Context()) + case OplServer: + return ts.Reg.OPLSyntaxRouter(t.Context()) + default: + panic(fmt.Sprintf("unknown ServerType %s", st)) + } +} diff --git a/cmd/client/grpc_client.go b/cmd/client/grpc_client.go new file mode 100644 index 0000000..c90a9f6 --- /dev/null +++ b/cmd/client/grpc_client.go @@ -0,0 +1,159 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package client + +import ( + "context" + "crypto/tls" + "fmt" + "os" + "strings" + "time" + + "golang.org/x/oauth2" + + "github.com/ory/x/flagx" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials/oauth" +) + +type contextKeys string + +const ( + FlagReadRemote = "read-remote" + FlagWriteRemote = "write-remote" + FlagOplRemote = "syntax-remote" + + FlagInsecureNoTransportSecurity = "insecure-disable-transport-security" + FlagInsecureSkipHostVerification = "insecure-skip-hostname-verification" + FlagAuthority = "authority" + FlagBlock = "block" + + EnvReadRemote = "KETO_READ_REMOTE" + EnvWriteRemote = "KETO_WRITE_REMOTE" + EnvAuthToken = "KETO_BEARER_TOKEN" //nolint:gosec // just the key, not the value + EnvAuthority = "KETO_AUTHORITY" + + ContextKeyTimeout contextKeys = "timeout" + ContextKeyDialFunc contextKeys = "dial" +) + +type connectionDetails struct { + token, authority string + skipHostVerification bool + noTransportSecurity bool + block bool +} + +func (d *connectionDetails) dialOptions() (opts []grpc.DialOption) { + if d.token != "" { + opts = append(opts, + grpc.WithPerRPCCredentials( + oauth.TokenSource{TokenSource: oauth2.StaticTokenSource(&oauth2.Token{AccessToken: d.token})}, + )) + } + if d.authority != "" { + opts = append(opts, grpc.WithAuthority(d.authority)) + } + + // TLS settings + switch { + case d.noTransportSecurity: + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + case d.skipHostVerification: + opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + //nolint:gosec // explicity set through scary flag + InsecureSkipVerify: true, + }))) + default: + // Defaults to the default host root CA bundle + opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(nil))) + } + if d.block { + opts = append(opts, grpc.WithBlock()) //nolint:staticcheck // SA1019 grpc.WithBlock is deprecated, but we actually want to block here until the connection is up. + } + return opts +} + +func getRemote(cmd *cobra.Command, flagRemote, envRemote string) (remote string) { + defer (func() { + if strings.HasPrefix(remote, "http://") || strings.HasPrefix(remote, "https://") { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "remote %q seems to be an http URL instead of a remote address\n", remote) + } + })() + + if cmd.Flags().Changed(flagRemote) { + return flagx.MustGetString(cmd, flagRemote) + } else if remote, isSet := os.LookupEnv(envRemote); isSet { + return remote + } + + // no value is set, use fallback from the flag and warn about that + remote = flagx.MustGetString(cmd, flagRemote) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "neither flag --%s nor env var %s are set, falling back to %s\n", flagRemote, envRemote, remote) + return remote +} + +func getAuthority(cmd *cobra.Command) string { + if cmd.Flags().Changed(FlagAuthority) { + return flagx.MustGetString(cmd, FlagAuthority) + } + return os.Getenv(EnvAuthority) +} + +func getConnectionDetails(cmd *cobra.Command) connectionDetails { + return connectionDetails{ + token: os.Getenv(EnvAuthToken), + authority: getAuthority(cmd), + skipHostVerification: flagx.MustGetBool(cmd, FlagInsecureSkipHostVerification), + noTransportSecurity: flagx.MustGetBool(cmd, FlagInsecureNoTransportSecurity), + block: flagx.MustGetBool(cmd, "block"), + } +} + +func GetReadConn(cmd *cobra.Command) (*grpc.ClientConn, error) { + return Conn(cmd.Context(), + getRemote(cmd, FlagReadRemote, EnvReadRemote), + getConnectionDetails(cmd), + ) +} + +func GetWriteConn(cmd *cobra.Command) (*grpc.ClientConn, error) { + return Conn(cmd.Context(), + getRemote(cmd, FlagWriteRemote, EnvWriteRemote), + getConnectionDetails(cmd), + ) +} + +func Conn(ctx context.Context, remote string, details connectionDetails) (*grpc.ClientConn, error) { + if d, ok := ctx.Value(ContextKeyTimeout).(time.Duration); ok { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, d) + defer cancel() + } + + if dial, ok := ctx.Value(ContextKeyDialFunc).(func(context.Context, string) (*grpc.ClientConn, error)); ok { + return dial(ctx, remote) + } + + return grpc.DialContext( // nolint:staticcheck // SA1019 grpc.DialContext is deprecated, but we actually want to connect here right away. + ctx, + remote, + details.dialOptions()..., + ) +} + +func RegisterRemoteURLFlags(flags *pflag.FlagSet) { + flags.String(FlagReadRemote, "127.0.0.1:4466", "Remote address of the read API endpoint.") + flags.String(FlagWriteRemote, "127.0.0.1:4467", "Remote address of the write API endpoint.") + flags.String(FlagAuthority, "", "Set the authority header for the remote gRPC server.") + flags.Bool(FlagInsecureNoTransportSecurity, false, "Disables transport security. Do not use this in production.") + flags.Bool(FlagInsecureSkipHostVerification, false, "Disables hostname verification. Do not use this in production.") + flags.Bool(FlagBlock, false, "Block until the connection is up.") +} diff --git a/cmd/client/grpc_client_test.go b/cmd/client/grpc_client_test.go new file mode 100644 index 0000000..3cdae1b --- /dev/null +++ b/cmd/client/grpc_client_test.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package client + +import ( + "bytes" + "os" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetRemote(t *testing.T) { + setup := func() (cmd *cobra.Command, stdOut, stdErr *bytes.Buffer) { + cmd = &cobra.Command{} + stdOut, stdErr = &bytes.Buffer{}, &bytes.Buffer{} + cmd.SetOut(stdOut) + cmd.SetErr(stdErr) + RegisterRemoteURLFlags(cmd.Flags()) + return + } + + setEnv := func(t *testing.T, env, val string) { + require.NoError(t, os.Setenv(env, val)) + t.Cleanup(func() { + require.NoError(t, os.Unsetenv(EnvReadRemote)) + }) + } + + t.Run("case=prefers flag value", func(t *testing.T) { + cmd, stdOut, stdErr := setup() + + expectedRemote := "ketotest.oryapis.com" + require.NoError(t, cmd.Flags().Set(FlagReadRemote, "ketotest.oryapis.com")) + setEnv(t, EnvReadRemote, "not"+expectedRemote) + + assert.Equal(t, expectedRemote, getRemote(cmd, FlagReadRemote, EnvReadRemote)) + assert.Equal(t, 0, stdOut.Len()) + assert.Equal(t, 0, stdErr.Len()) + }) + + t.Run("case=uses env value", func(t *testing.T) { + cmd, stdOut, stdErr := setup() + + expectedRemote := "ketotest.oryapis.com" + setEnv(t, EnvReadRemote, expectedRemote) + + assert.Equal(t, expectedRemote, getRemote(cmd, FlagReadRemote, EnvReadRemote)) + assert.Equal(t, 0, stdOut.Len()) + assert.Equal(t, 0, stdErr.Len()) + }) + + t.Run("case=falls back to flag default and prints warning", func(t *testing.T) { + cmd, stdOut, stdErr := setup() + + expectedRemote := cmd.Flags().Lookup(FlagReadRemote).DefValue + + assert.Equal(t, expectedRemote, getRemote(cmd, FlagReadRemote, EnvReadRemote)) + assert.Equal(t, 0, stdOut.Len()) + assert.Contains(t, stdErr.String(), "falling back") + }) +} diff --git a/cmd/expand/root.go b/cmd/expand/root.go new file mode 100644 index 0000000..d159cd7 --- /dev/null +++ b/cmd/expand/root.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand + +import ( + "fmt" + + "github.com/ory/x/cmdx" + "github.com/spf13/cobra" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/cmd/helpers" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +const FlagMaxDepth = "max-depth" + +func NewExpandCmd() *cobra.Command { + var maxDepth int32 + + cmd := &cobra.Command{ + Use: "expand :", + Short: "Expand all subjects that have the relation on the object", + Long: "Expand all subjects that have the relation on the object, including how access is granted.\n\n" + + "Example:\n" + + " keto expand view Doc:readme\n\n", + Args: cobra.RangeArgs(2, 3), + RunE: func(cmd *cobra.Command, args []string) error { + namespace, object, err := helpers.ParseNamespaceObject(cmd, args[1:]) + if err != nil { + return err + } + + conn, err := client.GetReadConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + cl := rts.NewExpandServiceClient(conn) + resp, err := cl.Expand(cmd.Context(), &rts.ExpandRequest{ + Subject: rts.NewSubjectSet(namespace, object, args[0]), + MaxDepth: maxDepth, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + var tree *ketoapi.Tree[*ketoapi.RelationTuple] + if resp.Tree != nil { + tree = ketoapi.TreeFromProto[*ketoapi.RelationTuple](resp.Tree) + } + + cmdx.PrintJSONAble(cmd, tree) + format, err := cmd.Flags().GetString(cmdx.FlagFormat) + if err != nil { + return err + } + quiet, err := cmd.Flags().GetBool(cmdx.FlagQuiet) + if err != nil { + return err + } + switch format { + case string(cmdx.FormatDefault), "": + if tree == nil && !quiet { + _, _ = fmt.Fprint(cmd.OutOrStdout(), "Got an empty tree. This probably means that the requested relation tuple is not present in Keto.") + } + _, _ = fmt.Fprintln(cmd.OutOrStdout()) + } + return nil + }, + } + + client.RegisterRemoteURLFlags(cmd.Flags()) + cmdx.RegisterJSONFormatFlags(cmd.Flags()) + cmdx.RegisterNoiseFlags(cmd.Flags()) + cmd.Flags().Int32VarP(&maxDepth, FlagMaxDepth, "d", 0, "Maximum depth of the tree to be returned. If the value is less than 1 or greater than the global max-depth then the global max-depth will be used instead.") + + return cmd +} + +func RegisterCommandsRecursive(parent *cobra.Command) { + parent.AddCommand(NewExpandCmd()) +} diff --git a/cmd/expand/root_test.go b/cmd/expand/root_test.go new file mode 100644 index 0000000..88c8de5 --- /dev/null +++ b/cmd/expand/root_test.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client/clienttest" + "github.com/ory/keto/cmd/relationtuple" + "github.com/ory/keto/internal/namespace" +) + +func TestExpandCommand(t *testing.T) { + nspace := &namespace.Namespace{Name: t.Name()} + nspaceUser := &namespace.Namespace{Name: "User"} + + newCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + } + RegisterCommandsRecursive(cmd) + relationtuple.RegisterCommandsRecursive(cmd) + return cmd + } + + ts := clienttest.NewTestServer(t, []*namespace.Namespace{nspace, nspaceUser}, newCmd) + + tuple := clienttest.RandomTupleWithSubjectSet(nspace.Name, nspaceUser.Name) + + nsObj := tuple.Namespace + ":" + tuple.Object + rel := tuple.Relation + subjectSet := tuple.SubjectSet.String() + + // create tuple with subjectSet + ts.Cmd.ExecNoErr(t, "relation-tuple", "create", subjectSet, rel, nsObj) + + t.Run("case=expands existing tuple as JSON", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "expand", rel, nsObj, + "--"+cmdx.FlagFormat, string(cmdx.FormatJSON)) + assert.Contains(t, stdOut, tuple.SubjectSet.Object) + }) + + t.Run("case=unknown tuple returns single node tree", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "expand", rel, nspace.Name+":unknown-obj", + "--"+cmdx.FlagFormat, string(cmdx.FormatJSON)) + assert.Contains(t, stdOut, rel) + assert.Contains(t, stdOut, nspace.Name) + }) + + t.Run("case=unknown tuple prints empty tree in default format", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t, "expand", rel, nspace.Name+":unknown-obj") + assert.Contains(t, stdOut, rel) + assert.Contains(t, stdOut, nspace.Name) + }) + + t.Run("case=3-arg format still works with deprecation warning", func(t *testing.T) { + stdOut, stdErr, err := ts.Cmd.Exec(nil, "expand", rel, nspace.Name, tuple.Object, + "--"+cmdx.FlagFormat, string(cmdx.FormatJSON)) + require.NoError(t, err) + assert.Contains(t, stdOut, tuple.SubjectSet.Object) + require.Contains(t, stdErr, "deprecated") + }) + + t.Run("case=errors on invalid namespace:object format", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "expand", rel, "no-colon-here") + require.Error(t, err) + require.Contains(t, stdErr, "expected : format") + }) +} diff --git a/cmd/helpers/helpers.go b/cmd/helpers/helpers.go new file mode 100644 index 0000000..fdc1320 --- /dev/null +++ b/cmd/helpers/helpers.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package helpers + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +func ParseSubject(s string) (*rts.Subject, error) { + if strings.Contains(s, ":") { + su, err := (&ketoapi.SubjectSet{}).FromString(s) + if err != nil { + return nil, err + } + + return rts.NewSubjectSet(su.Namespace, su.Object, su.Relation), nil + } + return rts.NewSubjectID(s), nil +} + +// ParseNamespaceObject parses namespace and object from args that may be in the +// combined format ["namespace:object"] or the legacy format ["namespace", "object"]. +// It writes a deprecation warning to cmd.ErrOrStderr if the legacy format is used. +func ParseNamespaceObject(cmd *cobra.Command, args []string) (namespace, object string, err error) { + switch len(args) { + case 2: + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Warning: passing namespace and object as separate arguments is deprecated. Use : instead.") + return args[0], args[1], nil + case 1: + namespace, object, ok := strings.Cut(args[0], ":") + // empty ObjectID is allowed + if !ok || namespace == "" { + return "", "", fmt.Errorf("expected : format, got %q", args[0]) + } + return namespace, object, nil + default: + return "", "", fmt.Errorf("unexpected number of arguments for :: got %d arguments - %s", len(args), strings.Join(args, ",")) + } +} diff --git a/cmd/helpers/helpers_test.go b/cmd/helpers/helpers_test.go new file mode 100644 index 0000000..6a8e9bb --- /dev/null +++ b/cmd/helpers/helpers_test.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package helpers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func TestParseSubject(t *testing.T) { + for _, tc := range []struct { + input string + expected *rts.Subject + }{ + { + input: "nspace:obj#rel", + expected: rts.NewSubjectSet("nspace", "obj", "rel"), + }, + { + input: "someid", + expected: rts.NewSubjectID("someid"), + }, + { + input: "nspace:id", + expected: rts.NewSubjectSet("nspace", "id", ""), + }, + } { + t.Run("subject="+tc.input, func(t *testing.T) { + actual, err := ParseSubject(tc.input) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/cmd/migrate/down.go b/cmd/migrate/down.go new file mode 100644 index 0000000..61ec516 --- /dev/null +++ b/cmd/migrate/down.go @@ -0,0 +1,86 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "fmt" + "strconv" + + "github.com/ory/x/cmdx" + "github.com/ory/x/flagx" + "github.com/ory/x/popx" + "github.com/spf13/cobra" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/ketoctx" +) + +func newDownCmd(opts []ketoctx.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "down ", + Short: "Migrate the database down", + Long: "Migrate the database down a specific amount of steps.\n" + + "Pass 0 steps to fully migrate down.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + steps, err := strconv.ParseInt(args[0], 0, 0) + if err != nil { + // return this error so it gets printed along the usage + return fmt.Errorf("malformed argument %s for : %+v", args[0], err) + } + + reg, err := driver.NewDefaultRegistry(cmd.Context(), cmd.Flags(), true, opts) + if err != nil { + return err + } + + conn, err := reg.PopConnection(cmd.Context()) + if err != nil { + return err + } + if err := popx.VerifyDialect(cmd.Context(), conn); err != nil { + return err + } + + mb, err := reg.MigrationBox(cmd.Context(), popx.WithoutGoldenDatabase()) + if err != nil { + return err + } + + return BoxDown(cmd, mb, int(steps)) + }, + } + + RegisterYesFlag(cmd.Flags()) + cmdx.RegisterFormatFlags(cmd.Flags()) + + return cmd +} + +func BoxDown(cmd *cobra.Command, mb *popx.MigrationBox, steps int) error { + s, err := mb.Status(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + cmdx.PrintTable(cmd, s) + + if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation("Do you really want to migrate down? This will delete data.", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") + return nil + } + + if err := mb.Down(cmd.Context(), steps); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could apply down migrations: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + s, err = mb.Status(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + cmdx.PrintTable(cmd, s) + return nil +} diff --git a/cmd/migrate/migrate_test.go b/cmd/migrate/migrate_test.go new file mode 100644 index 0000000..7091f01 --- /dev/null +++ b/cmd/migrate/migrate_test.go @@ -0,0 +1,181 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "bytes" + "context" + "strings" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/dbal" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/x/dbx" +) + +func assertAllApplied(t *testing.T, status string) { + assert.NotContains(t, status, "Pending") + assert.Contains(t, status, "Applied") +} + +func assertNoneApplied(t *testing.T, status string) { + assert.Contains(t, status, "Pending") + assert.NotContains(t, status, "Applied") +} + +func TestMigrate(t *testing.T) { + t.Parallel() + + nspaces := []*namespace.Namespace{ + { + Name: "default", + }, + { + Name: "other", + }, + } + + newCmd := func(ctx context.Context, persistentArgs ...string) *cmdx.CommandExecuter { + return &cmdx.CommandExecuter{ + New: func() *cobra.Command { + cmd := NewMigrateCmd(nil) + configx.RegisterFlags(cmd.PersistentFlags()) + return cmd + }, + Ctx: ctx, + PersistentArgs: persistentArgs, + } + } + + for _, dsn := range dbx.GetDSNs(t) { + if dbal.IsMemorySQLite(dsn.Conn) { + t.Run("dsn=memory", func(t *testing.T) { + t.Parallel() + + t.Run("case=auto migrates", func(t *testing.T) { + cf := dbx.ConfigFile(t, map[string]interface{}{ + config.KeyDSN: dsn.Conn, + config.KeyNamespaces: nspaces, + "log.level": "debug", + }) + + cmd := newCmd(t.Context(), "-c", cf) + + out := cmd.ExecNoErr(t, "up", "--"+FlagYes) + assert.Contains(t, out, "All migrations are already applied, there is nothing to do.") + }) + }) + } else { + t.Run("dsn="+dsn.Name, func(t *testing.T) { + t.Parallel() + + cf := dbx.ConfigFile(t, map[string]interface{}{ + config.KeyDSN: dsn.Conn, + config.KeyNamespaces: nspaces, + "log.level": "debug", + }) + + cmd := newCmd(t.Context(), "-c", cf) + + t.Run("case=shows status", func(t *testing.T) { + stdOut := cmd.ExecNoErr(t, "status") + assert.Contains(t, stdOut, "Pending") + assert.NotContains(t, stdOut, "Applied") + }) + + t.Run("case=status blocks until all are applied", func(t *testing.T) { + cmd := *cmd + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + cmd.Ctx = ctx + + stdErr := &bytes.Buffer{} + stdOut := &cmdx.CallbackWriter{ + Callbacks: map[string]func([]byte) error{ + "Waiting for migrations to finish...": func([]byte) error { + cancel() + return nil + }, + }, + } + + err := cmd.ExecBackground(nil, stdOut, stdErr, "status", "--block").Wait() + require.ErrorIs(t, err, cmdx.ErrNoPrintButFail) + assert.Contains(t, stdOut.String(), "Waiting for migrations to finish...") + assert.Contains(t, stdErr.String(), "Context was canceled, exiting...", stdOut.String()) + }) + + t.Run("case=aborts on no", func(t *testing.T) { + stdOut, stdErr, err := cmd.Exec(strings.NewReader("n\n"), "up") + require.NoError(t, err, "%s %s", stdOut, stdErr) + + assert.Containsf(t, stdOut, "Pending", "%s %s", stdOut, stdErr) + assert.NotContainsf(t, stdOut, "Applied", "%s %s", stdOut, stdErr) + assert.Containsf(t, stdOut, "Aborting", "%s %s", stdOut, stdErr) + }) + + t.Run("case=applies on yes input", func(t *testing.T) { + stdOut, stdErr, err := cmd.Exec(bytes.NewBufferString("y\n"), "up") + require.NoErrorf(t, err, "%s %s", stdOut, stdErr) + + t.Cleanup(func() { + // migrate all down + t.Logf("cleanup:\n%s\n", cmd.ExecNoErr(t, "down", "0", "--"+FlagYes)) + }) + + parts := strings.Split(stdOut, "Are you sure that you want to apply this migration?") + require.Len(t, parts, 2) + + assertNoneApplied(t, parts[0]) + assertAllApplied(t, parts[1]) + }) + + t.Run("case=applies on yes flag", func(t *testing.T) { + out := cmd.ExecNoErr(t, "up", "--"+FlagYes) + + t.Cleanup(func() { + // migrate all down + t.Logf("cleanup:\n%s\n", cmd.ExecNoErr(t, "down", "0", "--"+FlagYes)) + }) + + parts := strings.Split(out, "Applying migrations...") + require.Len(t, parts, 2) + + assertNoneApplied(t, parts[0]) + assertAllApplied(t, parts[1]) + }) + }) + } + } +} + +func TestUpAndDown(t *testing.T) { + t.Parallel() + + newCmd := func() *cobra.Command { + cmd := NewMigrateCmd(nil) + configx.RegisterFlags(cmd.PersistentFlags()) + return cmd + } + for _, dsn := range dbx.GetDSNs(t) { + t.Run("dsn="+dsn.Name, func(t *testing.T) { + cf := dbx.ConfigFile(t, map[string]interface{}{ + config.KeyDSN: dsn.Conn, + config.KeyNamespaces: []*namespace.Namespace{}, + }) + + t.Log(cmdx.ExecNoErr(t, newCmd(), "up", "-c", cf, "--"+FlagYes)) + t.Log(cmdx.ExecNoErr(t, newCmd(), "down", "0", "-c", cf, "--"+FlagYes)) + }) + } +} diff --git a/cmd/migrate/root.go b/cmd/migrate/root.go new file mode 100644 index 0000000..53dfcb9 --- /dev/null +++ b/cmd/migrate/root.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "github.com/spf13/cobra" + + "github.com/ory/keto/ketoctx" +) + +func NewMigrateCmd(opts []ketoctx.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate", + Short: "Commands to migrate the database", + Long: "Commands to migrate the database.\n" + + "This does not affect namespaces. Use `keto namespace migrate` for migrating namespaces.", + } + cmd.AddCommand( + newStatusCmd(opts), + newUpCmd(opts), + newDownCmd(opts), + ) + return cmd +} + +func RegisterCommandsRecursive(parent *cobra.Command, opts []ketoctx.Option) { + parent.AddCommand(NewMigrateCmd(opts)) +} diff --git a/cmd/migrate/status.go b/cmd/migrate/status.go new file mode 100644 index 0000000..3ba1623 --- /dev/null +++ b/cmd/migrate/status.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "fmt" + "time" + + "github.com/ory/x/popx" + + "github.com/ory/x/cmdx" + "github.com/spf13/cobra" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/ketoctx" +) + +func newStatusCmd(opts []ketoctx.Option) *cobra.Command { + block := false + cmd := &cobra.Command{ + Use: "status", + Short: "Get the current migration status", + Long: "Get the current migration status.\n" + + "This does not affect namespaces. Use `keto namespace migrate status` for migrating namespaces.", + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + reg, err := driver.NewDefaultRegistry(ctx, cmd.Flags(), true, opts) + if err != nil { + return err + } + + conn, err := reg.PopConnection(ctx) + if err != nil { + return err + } + if err := popx.VerifyDialect(ctx, conn); err != nil { + return err + } + + mb, err := reg.MigrationBox(ctx) + if err != nil { + return err + } + + s, err := mb.Status(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + for block && s.HasPending() { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for migrations to finish...\n") + for _, m := range s { + if m.State == popx.Pending { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", m.Name) + } + } + select { + case <-ctx.Done(): + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Context was canceled, exiting...") + return cmdx.FailSilently(cmd) + case <-time.After(time.Second): + } + s, err = mb.Status(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + cmdx.PrintTable(cmd, s) + return nil + }, + } + + cmdx.RegisterFormatFlags(cmd.Flags()) + cmd.Flags().BoolVar(&block, "block", false, "Block until all migrations have been applied") + + return cmd +} diff --git a/cmd/migrate/up.go b/cmd/migrate/up.go new file mode 100644 index 0000000..c981662 --- /dev/null +++ b/cmd/migrate/up.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrate + +import ( + "fmt" + + "github.com/ory/x/cmdx" + "github.com/ory/x/flagx" + "github.com/ory/x/popx" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/ketoctx" +) + +const ( + FlagYes = "yes" +) + +func newUpCmd(opts []ketoctx.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "up", + Short: "Migrate the database up", + Long: `Run this command on a fresh SQL installation and when you upgrade Ory Keto from version v0.7.x and later. + +It is recommended to run this command close to the SQL instance (e.g. same subnet) instead of over the public internet. +This decreases risk of failure and decreases time required. + +### WARNING ### + +Before running this command on an existing database, create a back up! +`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + + reg, err := driver.NewDefaultRegistry(ctx, cmd.Flags(), true, opts) + if err != nil { + return err + } + + conn, err := reg.PopConnection(ctx) + if err != nil { + return err + } + if err := popx.VerifyDialect(ctx, conn); err != nil { + return err + } + + mb, err := reg.MigrationBox(ctx, popx.WithoutGoldenDatabase()) + if err != nil { + return err + } + + if err := BoxUp(cmd, mb); err != nil { + return err + } + + return nil + }, + } + + RegisterYesFlag(cmd.Flags()) + + cmdx.RegisterFormatFlags(cmd.Flags()) + + return cmd +} + +func RegisterYesFlag(flags *pflag.FlagSet) { + flags.BoolP(FlagYes, "y", false, "yes to all questions, no user input required") +} + +func BoxUp(cmd *cobra.Command, mb *popx.MigrationBox) error { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Current status:") + + s, err := mb.Status(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + cmdx.PrintTable(cmd, s) + + if !s.HasPending() { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "All migrations are already applied, there is nothing to do.") + return nil + } + + if !flagx.MustGetBool(cmd, FlagYes) && !cmdx.AskForConfirmation("Are you sure that you want to apply this migration? Make sure to check the CHANGELOG.md for breaking changes beforehand.", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Aborting") + return nil + } + + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Applying migrations...") + + if err := mb.Up(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied all migrations:") + + s, err = mb.Status(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + cmdx.PrintTable(cmd, s) + return nil +} diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json new file mode 100644 index 0000000..f278cfa --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=empty.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`" diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json new file mode 100644 index 0000000..428d7b6 --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=many.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`\nclass one implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n\nclass two implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n\nclass three implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n" diff --git a/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json new file mode 100644 index 0000000..59cea50 --- /dev/null +++ b/cmd/namespace/.snapshots/TestGenerateOPLConfig-case=one.json @@ -0,0 +1 @@ +"import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'\n\n// Declare new namespaces as classes that implement `Namespace`\nclass one implements Namespace {\n related: {\n // Define relations to other objects here.\n // Examples:\n //\n // parents: (File | Folder)[]\n // viewers: SubjectSet\u003cGroup, \"members\"\u003e[]\n }\n\n permits = {\n // Define permissions here. These can be derived from the relations above.\n // Examples:\n //\n // view: (ctx: Context): boolean =\u003e\n // this.related.viewers.includes(ctx.subject) ||\n // this.related.parents.traverse((p) =\u003e p.permits.view(ctx)),\n }\n}\n" diff --git a/cmd/namespace/config_template/namespaces.ts.tmpl b/cmd/namespace/config_template/namespaces.ts.tmpl new file mode 100644 index 0000000..b06dbd8 --- /dev/null +++ b/cmd/namespace/config_template/namespaces.ts.tmpl @@ -0,0 +1,22 @@ +import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types' + +// Declare new namespaces as classes that implement `Namespace`{{ range .Namespaces }} +class {{ . }} implements Namespace { + related: { + // Define relations to other objects here. + // Examples: + // + // parents: (File | Folder)[] + // viewers: SubjectSet[] + } + + permits = { + // Define permissions here. These can be derived from the relations above. + // Examples: + // + // view: (ctx: Context): boolean => + // this.related.viewers.includes(ctx.subject) || + // this.related.parents.traverse((p) => p.permits.view(ctx)), + } +} +{{ end }} \ No newline at end of file diff --git a/cmd/namespace/opl_generate.go b/cmd/namespace/opl_generate.go new file mode 100644 index 0000000..88ab3d0 --- /dev/null +++ b/cmd/namespace/opl_generate.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "embed" + "io" + "text/template" + + "github.com/pkg/errors" +) + +//go:embed config_template/* +var configTemplate embed.FS + +// GenerateOPLConfig derives an Ory Permission Language config from the +// namespaces and writes it to out. The OPL config is functionally equivalent to +// the list of namespaces. +func GenerateOPLConfig(namespaces []string, out io.Writer) error { + t, err := template.New("config_template").ParseFS(configTemplate, "config_template/*") + if err != nil { + return errors.WithStack(err) + } + return errors.WithStack(t.ExecuteTemplate(out, + "namespaces.ts.tmpl", + struct{ Namespaces []string }{Namespaces: namespaces})) +} diff --git a/cmd/namespace/opl_generate_test.go b/cmd/namespace/opl_generate_test.go new file mode 100644 index 0000000..26f48fa --- /dev/null +++ b/cmd/namespace/opl_generate_test.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace_test + +import ( + "bytes" + "testing" + + "github.com/ory/x/snapshotx" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/namespace" +) + +func TestGenerateOPLConfig(t *testing.T) { + cases := []struct { + name string + namespaces []string + }{{ + name: "empty", + namespaces: []string{}, + }, { + name: "one", + namespaces: []string{"one"}, + }, { + name: "many", + namespaces: []string{"one", "two", "three"}, + }} + + for _, tc := range cases { + t.Run("case="+tc.name, func(t *testing.T) { + var out bytes.Buffer + require.NoError(t, namespace.GenerateOPLConfig(tc.namespaces, &out)) + snapshotx.SnapshotT(t, out.String()) + }) + } +} diff --git a/cmd/namespace/root.go b/cmd/namespace/root.go new file mode 100644 index 0000000..24a8fd9 --- /dev/null +++ b/cmd/namespace/root.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "github.com/spf13/cobra" + + "github.com/ory/keto/ketoctx" +) + +func NewNamespaceCmd() *cobra.Command { + return &cobra.Command{ + Use: "namespace", + Short: "Read and manipulate namespaces", + } +} + +func RegisterCommandsRecursive(parent *cobra.Command, _ []ketoctx.Option) { + rootCmd := NewNamespaceCmd() + rootCmd.AddCommand(NewValidateCmd()) + + parent.AddCommand(rootCmd) +} diff --git a/cmd/namespace/validate.go b/cmd/namespace/validate.go new file mode 100644 index 0000000..50451d8 --- /dev/null +++ b/cmd/namespace/validate.go @@ -0,0 +1,206 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "github.com/segmentio/objconv/yaml" + "github.com/spf13/cobra" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/jsonschemax" + "github.com/ory/x/logrusx" + + "github.com/ory/keto/embedx" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" +) + +func NewValidateCmd() *cobra.Command { + cmd := &cobra.Command{ + Deprecated: "The legacy namespaces are deprecated. Please use the new Ory Permission Language instead.", + Aliases: []string{"validate"}, + Use: "validate-legacy [ ...] | validate -c ", + Short: "Validate legacy namespace definitions", + Long: `validate-legacy +Validates namespace definitions. Parses namespace yaml files or configuration +files passed via the configuration flag. Returns human readable errors. Useful for +debugging.`, + RunE: func(cmd *cobra.Command, args []string) error { + cfFlag := cmd.Flag(configx.FlagConfig) + if cfFlag.Changed { + cfiles, err := cmd.Flags().GetStringSlice(configx.FlagConfig) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Failed to read config command line flag\n%+v\n", err) + return cmdx.FailSilently(cmd) + } + for _, fn := range cfiles { + err := validateConfigFile(cmd, fn) + if err != nil { + return err + } + } + } + + // user passed a list of namespace files + for _, fn := range args { + _, err := validateNamespaceFile(cmd, fn) + if err != nil { + return err + } + } + + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Congrats, all files are valid!") + return nil + }, + } + + return cmd +} + +var configSchema *jsonschema.Schema + +const schemaPath = "github.com/ory/keto/.schema/config.schema.json" + +func getSchema(cmd *cobra.Command) (*jsonschema.Schema, error) { + if configSchema == nil { + c := jsonschema.NewCompiler() + if err := c.AddResource(schemaPath, bytes.NewBuffer(embedx.ConfigSchema)); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not add the config schema file to the compiler. This is an internal error that should be reported. Thanks ;)\n%+v\n", err) + return nil, cmdx.FailSilently(cmd) + } + + var err error + configSchema, err = c.Compile(cmd.Context(), schemaPath+"#/definitions/namespace") + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not compile the config schema file. This is an internal error that should be reported. Thanks ;)\n%+v\n", err) + return nil, cmdx.FailSilently(cmd) + } + } + return configSchema, nil +} + +func validateNamespaceFile(cmd *cobra.Command, fn string) (*namespace.Namespace, error) { + cleanName := filepath.Clean(fn) + fc, err := os.ReadFile(cleanName) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not read file %q: %+v\n", fn, err) + return nil, cmdx.FailSilently(cmd) + } + + parse, err := config.GetParser(cleanName) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to infer file type from %q: %+v\n", fn, err) + return nil, cmdx.FailSilently(cmd) + } + + return validateNamespaceBytes(cmd, cleanName, fc, parse) +} + +func validateNamespaceBytes(cmd *cobra.Command, name string, b []byte, parser config.Parser) (*namespace.Namespace, error) { + schema, err := getSchema(cmd) + if err != nil { + return nil, err + } + + var val map[string]any + if err := parser(b, &val); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Encountered unmarshal error for %q: %+v\n", name, err) + return nil, cmdx.FailSilently(cmd) + } + + if err := schema.ValidateInterface(val); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "File %s was not a valid namespace file. Reasons:\n", name) + jsonschemax.FormatValidationErrorForCLI(cmd.ErrOrStderr(), embedx.ConfigSchema, err) + return nil, cmdx.FailSilently(cmd) + } + + var n namespace.Namespace + if err := parser(b, &n); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Encountered unmarshal error for %q: %+v\n", name, err) + return nil, cmdx.FailSilently(cmd) + } + + return &n, nil +} + +func validateConfigFile(cmd *cobra.Command, fn string) error { + cleanName := filepath.Clean(fn) + fc, err := os.ReadFile(cleanName) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not read file %q: %+v\n", fn, err) + return cmdx.FailSilently(cmd) + } + + parse, err := config.GetParser(cleanName) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to infer file type from %q: %+v\n", fn, err) + return cmdx.FailSilently(cmd) + } + + var val map[string]any + if err := parse(fc, &val); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Encountered parse error for %q: %+v\n", fn, err) + return cmdx.FailSilently(cmd) + } + + if ns, ok := val["namespaces"]; ok { + // ns can either be a file url or a list of namespace objects + switch t := ns.(type) { + case string: + logger := logrusx.New("cmd", "0") + cw, err := config.NewNamespaceWatcher(cmd.Context(), logger, t) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Encountered error reading config: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + files := cw.NamespaceFiles() + for _, file := range files { + _, err := validateNamespaceBytes(cmd, file.Name, file.Contents, file.Parser) + if err != nil { + return err + } + } + return nil + case []any: + for i, obj := range t { + fc, err := yaml.Marshal(obj) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Internal error. Failed to marshal yaml %+v\n", err) + return cmdx.FailSilently(cmd) + } + _, err = validateNamespaceBytes(cmd, fmt.Sprintf("index: %d", i), fc, yaml.Unmarshal) + if err != nil { + return err + } + } + return nil + case []map[string]any: + for i, obj := range t { + fc, err := yaml.Marshal(obj) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Internal error. Failed to marshal yaml %+v\n", err) + return cmdx.FailSilently(cmd) + } + _, err = validateNamespaceBytes(cmd, fmt.Sprintf("index: %d", i), fc, yaml.Unmarshal) + if err != nil { + return err + } + } + return nil + default: + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "unknown type %T for key 'namespaces' in config file: %v\n", t, t) + return cmdx.FailSilently(cmd) + } + } else { + return nil + } +} diff --git a/cmd/namespace/validate_test.go b/cmd/namespace/validate_test.go new file mode 100644 index 0000000..2827e0b --- /dev/null +++ b/cmd/namespace/validate_test.go @@ -0,0 +1,158 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const fileMode = 0644 + +const ( + configEmbeddedYaml = ` +dsn: memory +namespaces: + - name: testns0 + id: 0 + - name: testns1 + id: 1 +` + + configEmbeddedJson = `{"dsn":"memory","namespaces":[{"name":"testns0","id":0}]}` + + configEmbeddedToml = ` +[[namespaces]] +name = "testns0" +id = 0 + +[[namespaces]] +name = "testns1" +id = 1 +` +) + +const configReference = ` +dsn: memory +namespaces: %s +` + +const nsyaml = "name: testns0\nid: 0" +const nsjson = `{"name": "testns1", "id": 1}` + +func TestValidateConfigNamespaces(t *testing.T) { + cmd := cmdx.CommandExecuter{New: validateCommand} + + t.Run("case=read valid config with embedded namespaces", func(t *testing.T) { + dir := t.TempDir() + fnyaml := filepath.Join(dir, "keto.yaml") + require.NoError(t, os.WriteFile(fnyaml, []byte(configEmbeddedYaml), fileMode)) + + fnjson := filepath.Join(dir, "keto.json") + require.NoError(t, os.WriteFile(fnjson, []byte(configEmbeddedJson), fileMode)) + + fntoml := filepath.Join(dir, "keto.toml") + require.NoError(t, os.WriteFile(fntoml, []byte(configEmbeddedToml), fileMode)) + + stdOut := cmd.ExecNoErr(t, "validate", "-c", fnyaml+","+fnjson+","+fntoml) + assert.Contains(t, stdOut, "Congrats, all files are valid!\n") + }) + + t.Run("case=supports 3 namespace file formats", func(t *testing.T) { + dir := t.TempDir() + nsfiles := map[string]string{ + filepath.Join(dir, "ns.yaml"): "name: testns0\nid: 0", + filepath.Join(dir, "ns.json"): "{\"name\": \"testns0\",\"id\": 0}", + filepath.Join(dir, "ns.toml"): "name = \"testns0\"\nid = 0", + } + for fn, contents := range nsfiles { + require.NoError(t, os.WriteFile(fn, []byte(contents), fileMode)) + } + + params := append([]string{"validate"}, keys(nsfiles)...) + cmd.ExecNoErr(t, params...) + }) + + t.Run("case=unknown namespace format gives error", func(t *testing.T) { + fn := filepath.Join(t.TempDir(), "ns.txt") + require.NoError(t, os.WriteFile(fn, []byte("name: ns\nid: 0"), fileMode)) + + _, stdErr, err := cmd.Exec(nil, "validate", fn) + require.ErrorIs(t, err, cmdx.ErrNoPrintButFail) + assert.Contains(t, stdErr, "Unable to infer file type") + }) + + t.Run("case=config passed as varg fails", func(t *testing.T) { + fn := filepath.Join(t.TempDir(), "keto.yaml") + require.NoError(t, os.WriteFile(fn, []byte(configEmbeddedYaml), fileMode)) + + // interprets config file as namespace file when `-c` flag is not passed + _, stdErr, err := cmd.Exec(nil, "validate", fn) + require.ErrorIs(t, err, cmdx.ErrNoPrintButFail) + assert.Regexp(t, "additionalProperties ((\"namespaces\", \"dsn\")|(\"dsn\", \"namespaces\")) not allowed", stdErr) + }) + + t.Run("case=read config with invalid embedded namespace", func(t *testing.T) { + fn := filepath.Join(t.TempDir(), "keto.yaml") + require.NoError(t, os.WriteFile(fn, []byte(`{"namespaces":[{"id":"x","name":"x"}]}`), fileMode)) + + _, stdErr, err := cmd.Exec(nil, "validate", "--config", fn) + require.ErrorIs(t, err, cmdx.ErrNoPrintButFail) + assert.Contains(t, stdErr, "not a valid namespace file") + assert.Contains(t, stdErr, "id:") + }) + + t.Run("case=read config with namespace as dir reference", func(t *testing.T) { + nsdir := t.TempDir() + fn := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(filepath.Join(nsdir, "ns0.yaml"), []byte(nsyaml), fileMode)) + require.NoError(t, os.WriteFile(filepath.Join(nsdir, "ns1.json"), []byte(nsjson), fileMode)) + require.NoError(t, os.WriteFile(fn, []byte(fmt.Sprintf(configReference, nsdir)), fileMode)) + + cmd.PersistentArgs = []string{} + stdOut := cmd.ExecNoErr(t, "validate", "-c", fn) + assert.Contains(t, stdOut, "Congrats, all files are valid!\n") + + stdOut = cmd.ExecNoErr(t, "validate", "-c", fmt.Sprintf("%s,%s", fn, fn)) + assert.Contains(t, stdOut, "Congrats, all files are valid!\n") + }) + + t.Run("case=read config with dir reference bad namespaces", func(t *testing.T) { + nsdir := t.TempDir() + fn := filepath.Join(t.TempDir(), "config.yaml") + require.NoError(t, os.WriteFile(filepath.Join(nsdir, "ns0.yaml"), []byte("name: badId\nid: foo"), fileMode)) + require.NoError(t, os.WriteFile(fn, []byte(fmt.Sprintf(configReference, nsdir)), fileMode)) + + cmd.PersistentArgs = []string{} + _, stdErr, err := cmd.Exec(nil, "validate", "-c", fn) + require.ErrorIs(t, err, cmdx.ErrNoPrintButFail) + assert.Contains(t, stdErr, "contains values or keys which are invalid") + }) +} + +func validateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + Short: "Global and consistent permission and authorization server", + } + configx.RegisterConfigFlag(cmd.PersistentFlags(), []string{}) + cmd.AddCommand(NewValidateCmd()) + return cmd +} + +func keys(m map[string]string) []string { + rv := make([]string, 0, len(m)) + for k := range m { + rv = append(rv, k) + } + return rv +} diff --git a/cmd/relationtuple/create.go b/cmd/relationtuple/create.go new file mode 100644 index 0000000..16003e8 --- /dev/null +++ b/cmd/relationtuple/create.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "github.com/spf13/cobra" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +const FlagFile = "file" + +func NewCreateCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "create : :", + Short: "Create relationship tuples from inline arguments or JSON files and folders", + Long: `Create relationship tuples from inline arguments or JSON files and folders. +Inline example: + keto relation-tuple create User:alice owner Doc:readme + +From file or folder: + keto relation-tuple create -f relationships1.json -f relationships2.json + keto relation-tuple create -f relationships-dir1 -f relationships-dir2 + +If a directory is provided, all JSON files inside it are processed. +Use '-' as filename to read from STD_IN: + keto relation-tuple create -f -`, + Args: cobra.ArbitraryArgs, + RunE: transactTuples(rts.RelationTupleDelta_ACTION_INSERT), + } + + registerFileFlag(cmd.Flags()) + registerPackageFlags(cmd.Flags()) + + return cmd +} diff --git a/cmd/relationtuple/create_test.go b/cmd/relationtuple/create_test.go new file mode 100644 index 0000000..7049d0d --- /dev/null +++ b/cmd/relationtuple/create_test.go @@ -0,0 +1,168 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/x/cmdx" + "github.com/ory/x/randx" + + "github.com/ory/keto/cmd/client/clienttest" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/ketoapi" +) + +func renderTable(table cmdx.Table) string { + var buf bytes.Buffer + cmdx.PrintTablef(&buf, table) + return buf.String() +} + +func TestCreateCmd(t *testing.T) { + nspace := &namespace.Namespace{Name: t.Name()} + nspaceUser := &namespace.Namespace{Name: "User"} + + newCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + } + RegisterCommandsRecursive(cmd) + return cmd + } + + otherNspace := &namespace.Namespace{Name: "other"} + ts := clienttest.NewTestServer(t, []*namespace.Namespace{nspace, nspaceUser, otherNspace}, newCmd) + + createFile := func(t *testing.T, tuple any) string { + dir := t.TempDir() + + data, err := json.Marshal(tuple) + require.NoError(t, err) + + filename := randx.MustString(10, randx.AlphaLowerNum) + ".json" + require.NoError(t, os.WriteFile(filepath.Join(dir, filename), data, 0o600)) + return filepath.Join(dir, filename) + } + + type getTuple func() *ketoapi.RelationTuple + + fns := map[string]getTuple{ + "subjectID": func() *ketoapi.RelationTuple { + return clienttest.RandomTupleWithSubjectID(nspace.Name) + }, + "subjectSet": func() *ketoapi.RelationTuple { + return clienttest.RandomTupleWithSubjectSet(nspace.Name, nspaceUser.Name) + }, + } + + for name, createTuple := range fns { + t.Run("case="+name, func(t *testing.T) { + t.Run("case=creates single tuple from files", func(t *testing.T) { + tuple1 := createTuple() + tuple2 := createTuple() + + tmpFile := createFile(t, tuple1) + tmpFile2 := createFile(t, []*ketoapi.RelationTuple{tuple2}) + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "create", "-f", tmpFile, "-f", tmpFile2) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut) + + // assert that we can `get` it + res1 := ts.Cmd.ExecNoErr(t, "relation-tuple", "get", "--namespace", tuple1.Namespace, "--object", tuple1.Object) + require.Equal(t, renderTable(&responseOutput{ + RelationTuples: NewAPICollection([]*ketoapi.RelationTuple{tuple1}), + IsLastPage: true, + }), res1) + + // assert that we can `get` it + res2 := ts.Cmd.ExecNoErr(t, "relation-tuple", "get", "--namespace", tuple2.Namespace, "--object", tuple2.Object) + require.Equal(t, renderTable(&responseOutput{ + RelationTuples: NewAPICollection([]*ketoapi.RelationTuple{tuple2}), + IsLastPage: true, + }), res2) + }) + + t.Run("case=creates tuples from files and stdin", func(t *testing.T) { + tuple1 := createTuple() + tupleStdin := createTuple() + + tupleFile := createFile(t, tuple1) + data, err := json.Marshal(tupleStdin) + require.NoError(t, err) + + stdOut, stdErr, err := ts.Cmd.Exec(bytes.NewReader(data), "relation-tuple", "create", "-f", "-", "-f", tupleFile) + require.NoError(t, err) + require.Empty(t, stdErr) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tupleStdin, tuple1})), stdOut) + }) + + t.Run("case=creates tuples from directory", func(t *testing.T) { + dir := t.TempDir() + + tuple1 := createTuple() + tuple2 := createTuple() + + data1, err := json.Marshal(tuple1) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple1.json"), data1, 0o600)) + + data2, err := json.Marshal(tuple2) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple2.json"), data2, 0o600)) + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "create", "-f="+dir) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut) + }) + + t.Run("case=creates inline tuple", func(t *testing.T) { + tuple := createTuple() + + subject := "" + if tuple.SubjectID != nil { + subject = *tuple.SubjectID + } else { + subject = tuple.SubjectSet.String() + } + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "create", subject, tuple.Relation, tuple.Namespace+":"+tuple.Object) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut) + }) + }) + } + + t.Run("case=fails on invalid JSON", func(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "invalid.json") + require.NoError(t, os.WriteFile(tmpFile, []byte("not valid json"), 0o600)) + + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "create", "-f="+tmpFile) + require.Error(t, err) + require.Contains(t, stdErr, "could not decode") + }) + + t.Run("case=fails on nonexistent file", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "create", "-f=/nonexistent/file.json") + require.Error(t, err) + require.Contains(t, stdErr, "error getting stats") + }) + + t.Run("case=fails on json file arguments without -f", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "create", "file1.json", "file2.json") + require.Error(t, err) + require.Contains(t, stdErr, "expected inline arguments or JSON files") + }) + + t.Run("case=missing colon between ns and obj", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "create", "sns:so1", "rel", "nsobj") + require.Error(t, err) + require.Contains(t, stdErr, "expected : format, got \"nsobj\"") + }) +} diff --git a/cmd/relationtuple/delete.go b/cmd/relationtuple/delete.go new file mode 100644 index 0000000..3c104ab --- /dev/null +++ b/cmd/relationtuple/delete.go @@ -0,0 +1,282 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/ory/x/cmdx" + "github.com/spf13/cobra" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/cmd/helpers" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +const ( + MaxFileSizeBytes = 10 * 1024 * 1024 // 10 MB + MaxDirDepth = 3 +) + +func NewDeleteCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete : :", + Short: "Delete relationship tuples from inline arguments or JSON files and folders", + Long: `Delete relationship tuples from inline arguments or JSON files and folders. +Inline example: + keto relation-tuple delete User:alice owner Doc:readme + +From file or folder: + keto relation-tuple delete -f relationships1.json -f relationships2.json + keto relation-tuple delete -f relationships-dir1 -f relationships-dir2 + +If a directory is provided, all JSON files inside it are processed. +Use '-' as filename to read from STD_IN: + keto relation-tuple delete -f -`, + Args: cobra.ArbitraryArgs, + RunE: transactTuples(rts.RelationTupleDelta_ACTION_DELETE), + } + + registerFileFlag(cmd.Flags()) + registerPackageFlags(cmd.Flags()) + + return cmd +} + +func transactTuples(action rts.RelationTupleDelta_Action) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + files, err := cmd.Flags().GetStringSlice(FlagFile) + if err != nil { + return err + } + if len(files) > 0 { + return transactTuplesFromFiles(cmd, action, files) + } + + if len(args) != 3 { + return fmt.Errorf("expected inline arguments or JSON files and folders") + } + + return transactTupleFromInlineArgs(cmd, action, args) + } +} + +func transactTupleFromInlineArgs(cmd *cobra.Command, action rts.RelationTupleDelta_Action, args []string) error { + namespace, object, err := helpers.ParseNamespaceObject(cmd, args[2:]) + if err != nil { + return err + } + + sub, err := helpers.ParseSubject(args[0]) + if err != nil { + return fmt.Errorf("could not parse subject %q: %w", args[0], err) + } + tuple := &rts.RelationTuple{ + Namespace: namespace, + Object: object, + Relation: args[1], + Subject: sub, + } + + conn, err := client.GetWriteConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + cl := rts.NewWriteServiceClient(conn) + _, err = cl.TransactRelationTuples(cmd.Context(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: []*rts.RelationTupleDelta{{ + Action: action, + RelationTuple: tuple, + }}, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + c, err := NewProtoCollection([]*rts.RelationTuple{tuple}) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "error printing the response: %s\n", err) + } + cmdx.PrintTable(cmd, c) + return nil +} + +func transactTuplesFromFiles(cmd *cobra.Command, action rts.RelationTupleDelta_Action, files []string) error { + conn, err := client.GetWriteConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + var tuples []*ketoapi.RelationTuple + var deltas []*rts.RelationTupleDelta + for _, fn := range files { + parsed, err := readTuplesFromPath(cmd, fn, 0, decodeTuples) + if err != nil { + // silencing the usage message as from here on, there can't be any syntax error in the command usage, + // but rather an error with the content of the files + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), err.Error()) + return cmdx.FailSilently(cmd) + } + for _, t := range parsed { + tuples = append(tuples, t) + deltas = append(deltas, &rts.RelationTupleDelta{ + Action: action, + RelationTuple: t.ToProto(), + }) + } + } + + cl := rts.NewWriteServiceClient(conn) + _, err = cl.TransactRelationTuples(cmd.Context(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + cmdx.PrintTable(cmd, NewAPICollection(tuples)) + return nil +} + +// TupleDecoder reads relation tuples from a reader. Implementations handle +// format-specific decoding (e.g. JSON, human-readable). +type TupleDecoder func(r io.Reader) ([]*ketoapi.RelationTuple, error) + +// readTuplesFromPath reads relation tuples from a file path, directory, or +// stdin (when path is "-"). The depth parameter tracks directory nesting to +// enforce MaxDirDepth. The decode function handles format-specific parsing. +func readTuplesFromPath(cmd *cobra.Command, path string, depth int, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) { + if path == "-" { + tuples, err := decode(cmd.InOrStdin()) + if err != nil { + return nil, fmt.Errorf("stdin: %w", err) + } + return tuples, nil + } + + cleanPath := filepath.Clean(path) + info, err := os.Stat(cleanPath) + if err != nil { + return nil, fmt.Errorf("error getting stats for %s: %w", path, err) + } + + if info.IsDir() { + if depth >= MaxDirDepth { + return nil, fmt.Errorf("maximum directory depth of %d exceeded at %s", MaxDirDepth, path) + } + tuples, err := readTuplesFromDir(cleanPath, decode) + if err != nil { + return nil, err + } + return tuples, nil + } + + return readTuplesFromFile(cleanPath, decode) +} + +func readTuplesFromDir(dir string, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) { + var tuples []*ketoapi.RelationTuple + + fsys := os.DirFS(dir) + err := fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return fmt.Errorf("walking %s: %w", path, walkErr) + } + + depth := 0 + if path != "." { + depth = strings.Count(path, "/") + 1 + } + + if depth > MaxDirDepth { + return fmt.Errorf("maximum directory depth of %d exceeded at %s", MaxDirDepth, path) + } + + if d.IsDir() { + return nil + } + + fullPath := filepath.Join(dir, filepath.FromSlash(path)) + + t, err := readTuplesFromFile(fullPath, decode) + if err != nil { + return err + } + tuples = append(tuples, t...) + return nil + }) + if err != nil { + return nil, err + } + return tuples, nil +} + +// readTuplesFromFile opens a single file and decodes its content using the +// provided decoder. +func readTuplesFromFile(path string, decode TupleDecoder) ([]*ketoapi.RelationTuple, error) { + f, err := os.Open(filepath.Clean(path)) + if err != nil { + return nil, fmt.Errorf("opening file %s: %w", path, err) + } + defer func() { _ = f.Close() }() + + tuples, err := decode(f) + if err != nil { + return nil, fmt.Errorf("%s: %w", path, err) + } + return tuples, nil +} + +// decodeTuples decodes JSON from the reader as either a single RelationTuple +// or an array of RelationTuples. Input is limited to MaxFileSizeBytes. +func decodeTuples(r io.Reader) ([]*ketoapi.RelationTuple, error) { + data, err := io.ReadAll(io.LimitReader(r, MaxFileSizeBytes+1)) + if err != nil { + return nil, fmt.Errorf("reading input: %w", err) + } + if len(data) == 0 { + return nil, fmt.Errorf("empty input") + } + if len(data) > MaxFileSizeBytes { + return nil, fmt.Errorf("input exceeds maximum size of %d bytes", MaxFileSizeBytes) + } + + // Trim UTF-8 BOM first, then leading/trailing whitespace + trimmed := bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF}) + trimmed = bytes.TrimSpace(trimmed) + + if len(trimmed) == 0 { + return nil, fmt.Errorf("empty input after trimming whitespace") + } + + decoder := json.NewDecoder(bytes.NewReader(trimmed)) + decoder.DisallowUnknownFields() + + if trimmed[0] == '[' { + var tuples []*ketoapi.RelationTuple + if err := decoder.Decode(&tuples); err != nil { + return nil, fmt.Errorf("could not decode: %w", err) + } + return tuples, nil + } + + var tuple ketoapi.RelationTuple + if err := decoder.Decode(&tuple); err != nil { + return nil, fmt.Errorf("could not decode: %w", err) + } + return []*ketoapi.RelationTuple{&tuple}, nil +} diff --git a/cmd/relationtuple/delete_all.go b/cmd/relationtuple/delete_all.go new file mode 100644 index 0000000..3557bbc --- /dev/null +++ b/cmd/relationtuple/delete_all.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +const ( + FlagForce = "force" +) + +func NewDeleteAllCmd() *cobra.Command { + var force bool + + cmd := &cobra.Command{ + Use: "delete-all", + Short: "Delete ALL relationships matching the relation query.", + Long: "Delete all relationships matching the relation query.\n" + + "It is recommended to first run the command without the `--force` flag to verify that the operation is safe.", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if !force { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "WARNING: This operation is not reversible. Please use the `--%s` flag to proceed.\nThese tuples would be deleted:\n\n", FlagForce) + return getTuples(new(int32(100)), new(""))(cmd, args) + } + + if cmd.Flags().Changed(FlagSubject) { + return fmt.Errorf("usage of --%s is not supported anymore, use --%s or --%s respectively", FlagSubject, FlagSubjectID, FlagSubjectSet) + } + + query, err := readQueryFromFlags(cmd) + if err != nil { + return err + } + + conn, err := client.GetWriteConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + cl := rts.NewWriteServiceClient(conn) + _, err = cl.DeleteRelationTuples(cmd.Context(), &rts.DeleteRelationTuplesRequest{ + RelationQuery: query, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + return nil + }, + } + registerPackageFlags(cmd.Flags()) + registerRelationTupleFlags(cmd.Flags()) + cmd.Flags().BoolVar(&force, FlagForce, false, "Force the deletion of relationships") + + return cmd +} diff --git a/cmd/relationtuple/delete_all_test.go b/cmd/relationtuple/delete_all_test.go new file mode 100644 index 0000000..b2314b1 --- /dev/null +++ b/cmd/relationtuple/delete_all_test.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "context" + "testing" + "time" + + "github.com/ory/x/cmdx" + "github.com/stretchr/testify/assert" +) + +func TestDeleteAllCmd(t *testing.T) { + t.Run("executes get instead of delete when run without force", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + cmd := NewDeleteAllCmd() + // we will get an error here because there is no server running, but we really only care about the execution path + stdout, _, _ := cmdx.ExecCtx(ctx, cmd, nil, "--insecure-skip-hostname-verification=true") + assert.Contains(t, stdout, "WARNING: This operation is not reversible.") + }) +} diff --git a/cmd/relationtuple/delete_test.go b/cmd/relationtuple/delete_test.go new file mode 100644 index 0000000..5e6d776 --- /dev/null +++ b/cmd/relationtuple/delete_test.go @@ -0,0 +1,236 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/client/clienttest" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/ketoapi" +) + +func TestDeleteCmd(t *testing.T) { + nspace := &namespace.Namespace{Name: t.Name()} + nspaceUser := &namespace.Namespace{Name: "User"} + + newCmd := func() *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + } + RegisterCommandsRecursive(cmd) + return cmd + } + + otherNspace := &namespace.Namespace{Name: "other"} + ts := clienttest.NewTestServer(t, []*namespace.Namespace{nspace, nspaceUser, otherNspace}, newCmd) + + // createTuple is a helper that creates a tuple via the create command so we can then delete it. + createTuple := func(t *testing.T, tuple *ketoapi.RelationTuple) { + t.Helper() + tmpFile := filepath.Join(t.TempDir(), "tuple.json") + data, err := json.Marshal(tuple) + require.NoError(t, err) + require.NoError(t, os.WriteFile(tmpFile, data, 0o600)) + ts.Cmd.ExecNoErr(t, "relation-tuple", "create", "-f="+tmpFile) + } + + type getTuple func() *ketoapi.RelationTuple + + fns := map[string]getTuple{ + "subjectID": func() *ketoapi.RelationTuple { + return clienttest.RandomTupleWithSubjectID(nspace.Name) + }, + "subjectSet": func() *ketoapi.RelationTuple { + return clienttest.RandomTupleWithSubjectSet(nspace.Name, nspaceUser.Name) + }, + } + + for name, makeTuple := range fns { + t.Run("case="+name, func(t *testing.T) { + t.Run("case=deletes single tuple from file", func(t *testing.T) { + tuple := makeTuple() + createTuple(t, tuple) + + tmpFile := filepath.Join(t.TempDir(), "tuple.json") + data, err := json.Marshal(tuple) + require.NoError(t, err) + require.NoError(t, os.WriteFile(tmpFile, data, 0o600)) + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut) + + // assert that the tuple is gone + res := ts.Cmd.ExecNoErr(t, "relation-tuple", "get", "--namespace", tuple.Namespace, "--object", tuple.Object) + require.Equal(t, renderTable(&responseOutput{ + RelationTuples: NewAPICollection(nil), + IsLastPage: true, + }), res) + }) + + t.Run("case=deletes tuples from stdin", func(t *testing.T) { + tuple := makeTuple() + createTuple(t, tuple) + + data, err := json.Marshal(tuple) + require.NoError(t, err) + + stdOut, stdErr, err := ts.Cmd.Exec(bytes.NewReader(data), "relation-tuple", "delete", "-f=-") + require.NoError(t, err) + require.Empty(t, stdErr) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut) + }) + + t.Run("case=deletes tuples from directory", func(t *testing.T) { + dir := t.TempDir() + + tuple1 := makeTuple() + tuple2 := makeTuple() + createTuple(t, tuple1) + createTuple(t, tuple2) + + data1, err := json.Marshal(tuple1) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple1.json"), data1, 0o600)) + + data2, err := json.Marshal(tuple2) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tuple2.json"), data2, 0o600)) + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+dir) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut) + }) + + t.Run("case=deletes inline tuple", func(t *testing.T) { + tuple := makeTuple() + createTuple(t, tuple) + + subject := "" + if tuple.SubjectID != nil { + subject = *tuple.SubjectID + } else { + subject = tuple.SubjectSet.String() + } + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", subject, tuple.Relation, tuple.Namespace+":"+tuple.Object) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut) + }) + }) + } + + t.Run("case=fails on invalid JSON", func(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "invalid.json") + require.NoError(t, os.WriteFile(tmpFile, []byte("not valid json"), 0o600)) + + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "-f="+tmpFile) + require.Error(t, err) + require.Contains(t, stdErr, "could not decode") + }) + + t.Run("case=fails on nonexistent file", func(t *testing.T) { + stdErr := ts.Cmd.ExecExpectedErr(t, "relation-tuple", "delete", "-f=/nonexistent/file.json") + require.Contains(t, stdErr, "error getting stats") + }) + + t.Run("case=fails on json file arguments without -f", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "file1.json", "file2.json") + require.Error(t, err) + require.NotEmpty(t, stdErr) + }) + + t.Run("case=missing colon between ns and obj", func(t *testing.T) { + _, stdErr, err := ts.Cmd.Exec(nil, "relation-tuple", "delete", "sns:so1", "rel", "nsobj") + require.Error(t, err) + require.Contains(t, stdErr, "expected : format, got \"nsobj\"") + }) + + t.Run("case=handles JSON with leading whitespace and BOM", func(t *testing.T) { + // Test with various prefixes: whitespace, BOM, and combination + prefixes := [][]byte{ + {'\n', '\n', ' ', ' ', '\t'}, + {0xEF, 0xBB, 0xBF}, + {0xEF, 0xBB, 0xBF, '\n', ' ', ' '}, + } + + for _, prefix := range prefixes { + // Test single tuple + tuple := clienttest.RandomTupleWithSubjectID(nspace.Name) + createTuple(t, tuple) + + tmpFile := filepath.Join(t.TempDir(), "tuple.json") + data, err := json.Marshal(tuple) + require.NoError(t, err) + dataWithPrefix := append(prefix, data...) + require.NoError(t, os.WriteFile(tmpFile, dataWithPrefix, 0o600)) + + stdOut := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple})), stdOut) + + // Test array + tuple1 := clienttest.RandomTupleWithSubjectID(nspace.Name) + tuple2 := clienttest.RandomTupleWithSubjectID(nspace.Name) + createTuple(t, tuple1) + createTuple(t, tuple2) + + tmpFile2 := filepath.Join(t.TempDir(), "tuples.json") + data2, err := json.Marshal([]*ketoapi.RelationTuple{tuple1, tuple2}) + require.NoError(t, err) + dataWithPrefix2 := append(prefix, data2...) + require.NoError(t, os.WriteFile(tmpFile2, dataWithPrefix2, 0o600)) + + stdOut2 := ts.Cmd.ExecNoErr(t, "relation-tuple", "delete", "-f="+tmpFile2) + require.Equal(t, renderTable(NewAPICollection([]*ketoapi.RelationTuple{tuple1, tuple2})), stdOut2) + } + }) +} + +func TestReadTuplesFromDir(t *testing.T) { + writeTuple := func(t *testing.T, dir string, name string) *ketoapi.RelationTuple { + t.Helper() + tuple := clienttest.RandomTupleWithSubjectID(t.Name()) + data, err := json.Marshal(tuple) + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(dir, name), data, 0o600)) + return tuple + } + + mkdirs := func(t *testing.T, root string, depth int) string { + t.Helper() + dir := root + for i := range depth { + dir = filepath.Join(dir, fmt.Sprintf("d%d", i)) + } + require.NoError(t, os.MkdirAll(dir, 0o750)) + return dir + } + + t.Run("case=succeeds at MaxDirDepth nested directories", func(t *testing.T) { + root := t.TempDir() + deepest := mkdirs(t, root, MaxDirDepth) + + // Place a file inside the second-to-last directory so its + // path-component depth equals MaxDirDepth. + tuple := writeTuple(t, filepath.Dir(deepest), "tuple.json") + + tuples, err := readTuplesFromDir(root, decodeTuples) + require.NoError(t, err) + require.Equal(t, []*ketoapi.RelationTuple{tuple}, tuples) + }) + + t.Run("case=fails at MaxDirDepth+1 nested directories", func(t *testing.T) { + root := t.TempDir() + mkdirs(t, root, MaxDirDepth+1) + + _, err := readTuplesFromDir(root, decodeTuples) + require.ErrorContains(t, err, "maximum directory depth") + }) +} diff --git a/cmd/relationtuple/get.go b/cmd/relationtuple/get.go new file mode 100644 index 0000000..f25d507 --- /dev/null +++ b/cmd/relationtuple/get.go @@ -0,0 +1,157 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +const ( + FlagNamespace = "namespace" + FlagSubject = "subject" + FlagSubjectID = "subject-id" + FlagSubjectSet = "subject-set" + FlagRelation = "relation" + FlagObject = "object" + FlagPageSize = "page-size" + FlagPageToken = "page-token" +) + +func registerRelationTupleFlags(flags *pflag.FlagSet) { + flags.String(FlagNamespace, "", "Set the requested namespace") + flags.String(FlagSubjectID, "", "Set the requested subject ID") + flags.String(FlagSubjectSet, "", `Set the requested subject set; format: "namespace:object#relation"`) + flags.String(FlagRelation, "", "Set the requested relation") + flags.String(FlagObject, "", "Set the requested object") + + flags.String(FlagSubject, "", "") + if err := flags.MarkHidden(FlagSubject); err != nil { + panic(err.Error()) + } +} + +func readQueryFromFlags(cmd *cobra.Command) (*rts.RelationQuery, error) { + getStringPtr := func(flagName string) *string { + if f := cmd.Flags().Lookup(flagName); f.Changed { + return new(f.Value.String()) + } + return nil + } + + query := &ketoapi.RelationQuery{ + Namespace: getStringPtr(FlagNamespace), + Object: getStringPtr(FlagObject), + Relation: getStringPtr(FlagRelation), + SubjectID: getStringPtr(FlagSubjectID), + } + if f := cmd.Flags().Lookup(FlagSubjectSet); f.Changed { + s, err := (&ketoapi.SubjectSet{}).FromString(f.Value.String()) + if err != nil { + return nil, err + } + query.SubjectSet = s + } + + return query.ToProto(), nil +} + +func NewGetCmd() *cobra.Command { + var ( + pageSize int32 + pageToken string + ) + + cmd := &cobra.Command{ + Use: "get", + Short: "Get relationships", + Long: "Get relationships matching the given partial tuple.\n" + + "Returns paginated results.", + Args: cobra.ExactArgs(0), + RunE: getTuples(&pageSize, &pageToken), + } + + registerPackageFlags(cmd.Flags()) + registerRelationTupleFlags(cmd.Flags()) + + cmd.Flags().StringVar(&pageToken, FlagPageToken, "", "page token acquired from a previous response") + cmd.Flags().Int32Var(&pageSize, FlagPageSize, 100, "maximum number of items to return") + + return cmd +} + +func getTuples(pageSize *int32, pageToken *string) func(cmd *cobra.Command, _ []string) error { + return func(cmd *cobra.Command, _ []string) error { + if cmd.Flags().Changed(FlagSubject) { + return fmt.Errorf("usage of --%s is not supported anymore, use --%s or --%s respectively", FlagSubject, FlagSubjectID, FlagSubjectSet) + } + + conn, err := client.GetReadConn(cmd) + if err != nil { + return err + } + defer func() { _ = conn.Close() }() + + cl := rts.NewReadServiceClient(conn) + query, err := readQueryFromFlags(cmd) + if err != nil { + return err + } + + resp, err := cl.ListRelationTuples(cmd.Context(), &rts.ListRelationTuplesRequest{ + RelationQuery: query, + PageSize: *pageSize, + PageToken: *pageToken, + }) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not make request: %s\n", err) + return cmdx.FailSilently(cmd) + } + + relationTuples, err := NewProtoCollection(resp.RelationTuples) + if err != nil { + return err + } + cmdx.PrintTable(cmd, &responseOutput{ + RelationTuples: relationTuples, + IsLastPage: resp.NextPageToken == "", + NextPageToken: resp.NextPageToken, + }) + return nil + } +} + +type responseOutput struct { + RelationTuples *Collection `json:"relation_tuples"` + IsLastPage bool `json:"is_last_page"` + NextPageToken string `json:"next_page_token"` +} + +func (r *responseOutput) Header() []string { + return r.RelationTuples.Header() +} + +func (r *responseOutput) Table() [][]string { + return append( + r.RelationTuples.Table(), + []string{}, + []string{"NEXT PAGE TOKEN", r.NextPageToken}, + []string{"IS LAST PAGE", strconv.FormatBool(r.IsLastPage)}, + ) +} + +func (r *responseOutput) Interface() any { return r } +func (r *responseOutput) Len() int { return r.RelationTuples.Len() + 3 } +func (r *responseOutput) IDs() []string { return r.RelationTuples.IDs() } + +var _ cmdx.Table = (*responseOutput)(nil) diff --git a/cmd/relationtuple/output.go b/cmd/relationtuple/output.go new file mode 100644 index 0000000..7e48df4 --- /dev/null +++ b/cmd/relationtuple/output.go @@ -0,0 +1,114 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "encoding/json" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +type ( + Collection struct { + apiRelations []*ketoapi.RelationTuple + } + OutputTuple struct { + *ketoapi.RelationTuple + } +) + +func NewProtoCollection(rels []*rts.RelationTuple) (*Collection, error) { + r := &Collection{apiRelations: make([]*ketoapi.RelationTuple, len(rels))} + for i, rel := range rels { + var err error + r.apiRelations[i], err = (&ketoapi.RelationTuple{}).FromDataProvider(rel) + if err != nil { + return nil, err + } + } + return r, nil +} + +func MustNewProtoCollection(rels []*rts.RelationTuple) *Collection { + c, err := NewProtoCollection(rels) + if err != nil { + panic(err) + } + return c +} + +func NewAPICollection(rels []*ketoapi.RelationTuple) *Collection { + return &Collection{apiRelations: rels} +} + +func (r *Collection) Header() []string { + return []string{ + "NAMESPACE", + "OBJECT", + "RELATION NAME", + "SUBJECT", + } +} + +func (r *Collection) Table() [][]string { + ir := r.apiRelations + + data := make([][]string, len(ir)) + for i, rel := range ir { + var sub string + if rel.SubjectID != nil { + sub = *rel.SubjectID + } else { + sub = rel.SubjectSet.String() + } + + data[i] = []string{rel.Namespace, rel.Object, rel.Relation, sub} + } + + return data +} + +func (r *Collection) Interface() any { return r.apiRelations } +func (r *Collection) UnmarshalJSON(raw []byte) error { return json.Unmarshal(raw, &r.apiRelations) } +func (r *Collection) Len() int { return len(r.apiRelations) } + +func (r *Collection) MarshalJSON() ([]byte, error) { + ir := r.apiRelations + return json.Marshal(ir) +} + +func (r *Collection) IDs() []string { + ts := r.apiRelations + ids := make([]string, len(ts)) + for i, rt := range ts { + ids[i] = rt.String() + } + return ids +} + +func (r *OutputTuple) Header() []string { + return []string{ + "NAMESPACE", + "OBJECT ID", + "RELATION NAME", + "SUBJECT", + } +} + +func (r *OutputTuple) Columns() []string { + return []string{ + r.Namespace, + r.Object, + r.Relation, + outputSubject(r.RelationTuple), + } +} + +func outputSubject(r *ketoapi.RelationTuple) string { + if r.SubjectID != nil { + return *r.SubjectID + } + return r.SubjectSet.String() +} diff --git a/cmd/relationtuple/output_test.go b/cmd/relationtuple/output_test.go new file mode 100644 index 0000000..11a2542 --- /dev/null +++ b/cmd/relationtuple/output_test.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +func TestRelationCollection(t *testing.T) { + t.Run("case=prints all", func(t *testing.T) { + expected := make([]*ketoapi.RelationTuple, 3) + for i := range expected { + expected[i] = &ketoapi.RelationTuple{ + Namespace: "n" + strconv.Itoa(i), + Object: "o" + strconv.Itoa(i), + Relation: "r" + strconv.Itoa(i), + SubjectID: new("s" + strconv.Itoa(i)), + } + } + expected[2].SubjectSet = &ketoapi.SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + } + expected[2].SubjectID = nil + + proto := make([]*rts.RelationTuple, 3) + for i := range expected { + proto[i] = &rts.RelationTuple{ + Namespace: "n" + strconv.Itoa(i), + Object: "o" + strconv.Itoa(i), + Relation: "r" + strconv.Itoa(i), + Subject: rts.NewSubjectID("s" + strconv.Itoa(i)), + } + } + proto[2].Subject = rts.NewSubjectSet("sn", "so", "sr") + + for i, c := range []*Collection{ + NewAPICollection(expected), + MustNewProtoCollection(proto), + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + var vals []string + for _, row := range c.Table() { + vals = append(vals, row...) + } + + ev := reflect.ValueOf(expected) + for el := 0; el < ev.Len(); el++ { + et := reflect.TypeOf(expected).Elem().Elem() + + for f := 0; f < et.NumField(); f++ { + v := ev.Index(el).Elem().Field(f) + // private field or nil + if !v.CanSet() || (v.Kind() == reflect.Pointer && v.IsNil()) { + continue + } + + switch k := v.Kind(); { + case k == reflect.String: + assert.Contains(t, vals, v.String()) + case k == reflect.Pointer && v.Elem().Kind() == reflect.String: + assert.Contains(t, vals, v.Elem().String()) + default: + t.Logf("unhandled kind %s %T", v.Kind(), v.Interface()) + str := v.MethodByName("String").Call(nil)[0].String() + assert.Contains(t, vals, str) + } + } + } + }) + } + }) + + t.Run("func=NewProtoCollection", func(t *testing.T) { + _, err := NewProtoCollection([]*rts.RelationTuple{{ /*subject is nil*/ }}) + require.ErrorIs(t, err, ketoapi.ErrNilSubject()) + }) + + t.Run("func=json", func(t *testing.T) { + ts := []*ketoapi.RelationTuple{ + { + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + { + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + }, + }, + } + expected, err := json.Marshal(ts) + require.NoError(t, err) + + collection := NewAPICollection(ts) + actual, err := json.Marshal(collection) + require.NoError(t, err) + assert.Equal(t, string(expected), string(actual)) + }) +} diff --git a/cmd/relationtuple/parse.go b/cmd/relationtuple/parse.go new file mode 100644 index 0000000..442fd16 --- /dev/null +++ b/cmd/relationtuple/parse.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "fmt" + "io" + "strings" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/ketoapi" +) + +func NewParseCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "parse", + Short: "Parse human readable relationships", + Long: "Parse human readable relationships as used in the documentation.\n" + + "Supports various output formats. Especially useful for piping into other commands by using `--format json`.\n" + + "Ignores comments (lines starting with `//`) and blank lines.\n\n" + + "From file or folder:\n" + + "\tketo relation-tuple parse -f tuples1.txt -f tuples2.txt\n" + + "\tketo relation-tuple parse -f tuples-dir\n\n" + + "Use '-' as filename to read from STD_IN:\n" + + "\tketo relation-tuple parse -f -", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + files, err := cmd.Flags().GetStringSlice(FlagFile) + if err != nil { + return err + } + if len(files) == 0 { + return fmt.Errorf("at least one file must be specified with -f") + } + + var rts []*ketoapi.RelationTuple + for _, fn := range files { + rtss, err := readTuplesFromPath(cmd, fn, 0, parseHumanReadable) + if err != nil { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), err.Error()) + return cmdx.FailSilently(cmd) + } + rts = append(rts, rtss...) + } + + if len(rts) == 1 { + cmdx.PrintRow(cmd, rts[0]) + return nil + } + cmdx.PrintTable(cmd, NewAPICollection(rts)) + return nil + }, + } + + registerFileFlag(cmd.Flags()) + cmdx.RegisterFormatFlags(cmd.Flags()) + + return cmd +} + +// parseHumanReadable reads human-readable relation tuples from the reader. +// Input is limited to MaxFileSizeBytes. +func parseHumanReadable(r io.Reader) ([]*ketoapi.RelationTuple, error) { + fc, err := io.ReadAll(io.LimitReader(r, int64(MaxFileSizeBytes)+1)) + if err != nil { + return nil, fmt.Errorf("reading input: %w", err) + } + if len(fc) > MaxFileSizeBytes { + return nil, fmt.Errorf("input exceeds maximum size of %d bytes", MaxFileSizeBytes) + } + + parts := strings.Split(string(fc), "\n") + rts := make([]*ketoapi.RelationTuple, 0, len(parts)) + for i, row := range parts { + row = strings.TrimSpace(row) + // ignore comments and empty lines + if row == "" || strings.HasPrefix(row, "//") { + continue + } + + rt, err := (&ketoapi.RelationTuple{}).FromString(row) + if err != nil { + return nil, fmt.Errorf("line %d: could not decode %q: %w", i+1, row, err) + } + rts = append(rts, rt) + } + + return rts, nil +} diff --git a/cmd/relationtuple/parse_test.go b/cmd/relationtuple/parse_test.go new file mode 100644 index 0000000..c92ef1c --- /dev/null +++ b/cmd/relationtuple/parse_test.go @@ -0,0 +1,132 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/ory/keto/ketoapi" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTuplesFromPath(t *testing.T) { + for _, tc := range []struct { + input, name string + expected []*ketoapi.RelationTuple + }{ + { + name: "single basic tuple", + input: "nspace:obj#rel@sub\n", + expected: []*ketoapi.RelationTuple{{ + Namespace: "nspace", + Object: "obj", + Relation: "rel", + SubjectID: new("sub"), + }}, + }, + { + name: "multiple tuples", + input: `nspace:obj1#rel@sub1 +nspace:obj2#rel@sub2 +nspace:obj2#rel@(nspace:obj2#rel)`, + expected: []*ketoapi.RelationTuple{ + { + Namespace: "nspace", + Object: "obj1", + Relation: "rel", + SubjectID: new("sub1"), + }, + { + Namespace: "nspace", + Object: "obj2", + Relation: "rel", + SubjectID: new("sub2"), + }, + { + Namespace: "nspace", + Object: "obj2", + Relation: "rel", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: "nspace", + Object: "obj2", + Relation: "rel", + }, + }, + }, + }, + { + name: "crap around tuples", + input: `// foo comment +nspace:obj#rel@sub + + // also indentation and trailing spaces + nspace:indent#rel@sub `, + expected: []*ketoapi.RelationTuple{ + { + Namespace: "nspace", + Object: "obj", + Relation: "rel", + SubjectID: new("sub"), + }, + { + Namespace: "nspace", + Object: "indent", + Relation: "rel", + SubjectID: new("sub"), + }, + }, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + cmd := &cobra.Command{} + cmd.SetIn(bytes.NewBufferString(tc.input)) + + actual, err := readTuplesFromPath(cmd, "-", 0, parseHumanReadable) + require.NoError(t, err) + assert.Equal(t, tc.expected, actual) + }) + } + + t.Run("case=error contains line number", func(t *testing.T) { + input := "nspace:obj#rel@subns:subid\n\n\ninvalid-line\nnspace:obj2#rel@subns:subid2\n" + cmd := &cobra.Command{} + cmd.SetIn(bytes.NewBufferString(input)) + + _, err := readTuplesFromPath(cmd, "-", 0, parseHumanReadable) + require.Error(t, err) + assert.Contains(t, err.Error(), "line 4:") + }) + + t.Run("case=reads from fs", func(t *testing.T) { + dir := t.TempDir() + fn := filepath.Join(dir, "test.tuples") + require.NoError(t, os.WriteFile(fn, []byte(` +nspace:obj1#rel@sub1 + +nspace:obj2#rel@sub2`), 0o600)) + + actual, err := readTuplesFromPath(&cobra.Command{}, fn, 0, parseHumanReadable) + require.NoError(t, err) + assert.Equal(t, []*ketoapi.RelationTuple{ + { + Namespace: "nspace", + Object: "obj1", + Relation: "rel", + SubjectID: new("sub1"), + }, + { + Namespace: "nspace", + Object: "obj2", + Relation: "rel", + SubjectID: new("sub2"), + }, + }, actual) + }) +} diff --git a/cmd/relationtuple/root.go b/cmd/relationtuple/root.go new file mode 100644 index 0000000..4c9427c --- /dev/null +++ b/cmd/relationtuple/root.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client" +) + +func newRelationCmd() *cobra.Command { + return &cobra.Command{ + Use: "relation-tuple", + Short: "Read and manipulate relationships", + } +} + +func RegisterCommandsRecursive(parent *cobra.Command) { + relationCmd := newRelationCmd() + + parent.AddCommand(relationCmd) + + relationCmd.AddCommand(NewGetCmd(), NewCreateCmd(), NewDeleteCmd(), NewDeleteAllCmd(), NewParseCmd()) +} + +func registerPackageFlags(flags *pflag.FlagSet) { + client.RegisterRemoteURLFlags(flags) + cmdx.RegisterFormatFlags(flags) +} + +func registerFileFlag(flags *pflag.FlagSet) { + flags.StringSliceP(FlagFile, "f", nil, "Read relationships from JSON files or directories (use the special filename - for stdin)") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..e7381c4 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + + "github.com/ory/keto/cmd/check" + "github.com/ory/keto/cmd/expand" + "github.com/ory/keto/cmd/migrate" + "github.com/ory/keto/cmd/namespace" + "github.com/ory/keto/cmd/relationtuple" + "github.com/ory/keto/cmd/server" + "github.com/ory/keto/cmd/status" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/ketoctx" +) + +func NewRootCmd(opts ...ketoctx.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "keto", + Short: "Global and consistent permission and authorization server", + } + + configx.RegisterConfigFlag(cmd.PersistentFlags(), []string{filepath.Join(userHomeDir(), "keto.yml")}) + + relationtuple.RegisterCommandsRecursive(cmd) + namespace.RegisterCommandsRecursive(cmd, opts) + migrate.RegisterCommandsRecursive(cmd, opts) + server.RegisterCommandsRecursive(cmd, opts) + check.RegisterCommandsRecursive(cmd) + expand.RegisterCommandsRecursive(cmd) + status.RegisterCommandRecursive(cmd) + + cmd.AddCommand(cmdx.Version(&config.Version, &config.Commit, &config.Date)) + + return cmd +} + +// Execute adds all child commands to the root command sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := NewRootCmd().ExecuteContext(ctx); err != nil { + if !errors.Is(err, cmdx.ErrNoPrintButFail) { + fmt.Println(err) + } + os.Exit(-1) + } +} + +func userHomeDir() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} diff --git a/cmd/serve_debug_test.go b/cmd/serve_debug_test.go new file mode 100644 index 0000000..f83e7bd --- /dev/null +++ b/cmd/serve_debug_test.go @@ -0,0 +1,177 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "testing" + "time" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/internal/driver" +) + +// This test is intended for debug purposes. Just comment out t.SkipNow() and run it through your IDE debugger with +func Test_DebugOnly(t *testing.T) { + t.SkipNow() + + ctx := context.Background() + err := cmdx.ExecBackgroundCtx( + ctx, + NewRootCmd(), + os.Stdin, + os.Stdout, + os.Stderr, + "serve", + "-c", + "../contrib/cat-videos-example/keto.yml", + ).Wait() + + t.Logf("got error %s", err) +} + +// This benchmark is intended for profiling specific operations +// +// Summary namespace manager reload: +// +// Memory manager: +// Mem: 4.35% of all while github.com/ory/x/configx.(*Provider).reload takes 93.56% +// CPU: 1.86% of all while github.com/ory/x/configx.(*Provider).reload takes 67.28% +// File manager: +// Mem: 0.83% of all while github.com/ory/x/configx.(*Provider).reload takes 96.34% +// CPU: 3.84% of all while github.com/ory/x/configx.(*Provider).reload takes 61.53% +func BenchmarkServe(b *testing.B) { + b.SkipNow() + + b.StopTimer() + h := test.Hook{} + + rCtx, cancel := context.WithCancel(context.WithValue(context.Background(), + driver.LogrusHookContextKey, &h)) + defer cancel() + + f, err := os.Create(filepath.Join(b.TempDir(), "keto.yml")) + require.NoError(b, err) + _, err = f.WriteString(` +log: + level: debug + +namespaces: + - id: 0 + name: videos + +dsn: memory`) + require.NoError(b, err) + require.NoError(b, f.Sync()) + + flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) + configx.RegisterFlags(flags) + require.NoError(b, flags.Set(configx.FlagConfig, f.Name())) + + reg, err := driver.NewDefaultRegistry(rCtx, flags, false, nil) + + // setting env vars instead of flags bc flags are not defined on every command + require.NoError(b, os.Setenv(client.EnvReadRemote, "127.0.0.1:4466")) + require.NoError(b, os.Setenv(client.EnvWriteRemote, "127.0.0.1:4467")) + + c := cmdx.CommandExecuter{ + New: func() *cobra.Command { + return NewRootCmd(nil) + }, + Ctx: context.WithValue(rCtx, driver.RegistryContextKey, reg), + } + stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} + eg := c.ExecBackground(nil, stdOut, stdErr, "serve", "-c", f.Name()) + b.Cleanup(func() { + require.NoError(b, eg.Wait()) + }) + require.Equal(b, "SERVING\n", c.ExecNoErr(b, "status", "-b")) + time.Sleep(100 * time.Millisecond) + + b.StartTimer() + + b.Run("memory namespace manager", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err = f.Seek(0, io.SeekStart) + require.NoError(b, err) + _, err = fmt.Fprintf(f, ` +log: + level: debug + +dsn: memory + +namespaces: + - id: %d + name: videos`, i) + require.NoError(b, err) + + var le *logrus.Entry + + for i := 0; i < 1000; i++ { + le = h.LastEntry() + if le != nil && le.Message == "Configuration change processed successfully." { + break + } + time.Sleep(1 * time.Millisecond) + } + require.NotNil(b, le, "iteration %d", i) + require.Equal(b, "Configuration change processed successfully.", le.Message) + + h.Reset() + } + }) + + b.Run("file namespace manager", func(b *testing.B) { + nn := []string{filepath.Join(b.TempDir(), "namespace.yml"), filepath.Join(b.TempDir(), "namespace.yml")} + for _, n := range nn { + require.NoError(b, os.WriteFile(n, []byte(` +id: 0 +name: foo`), 0600)) + } + + for i := 0; i < b.N; i++ { + _, err = f.Seek(0, io.SeekStart) + require.NoError(b, err) + _, err = fmt.Fprintf(f, ` +log: + level: debug + +dsn: memory + +namespaces: file://%s + +# foo bar this is only for ensuring the old values are not around`, nn[i%2]) + require.NoError(b, err) + require.NoError(b, f.Sync()) + + var le *logrus.Entry + + for i := 0; i < 1000; i++ { + le = h.LastEntry() + if le != nil && le.Message == "Configuration change processed successfully." { + break + } + time.Sleep(1 * time.Millisecond) + } + require.NotNil(b, le) + require.Equal(b, "Configuration change processed successfully.", le.Message) + + h.Reset() + } + }) +} diff --git a/cmd/server/registry.go b/cmd/server/registry.go new file mode 100644 index 0000000..3ac03f8 --- /dev/null +++ b/cmd/server/registry.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "errors" + "fmt" + + "github.com/spf13/cobra" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/cmdx" + "github.com/ory/x/popx" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/persistence" + "github.com/ory/keto/ketoctx" +) + +func NewRegistry(cmd *cobra.Command, opts []ketoctx.Option) (driver.Registry, error) { + reg, err := driver.NewDefaultRegistry(cmd.Context(), cmd.Flags(), false, opts) + if errors.Is(err, persistence.ErrNetworkMigrationsMissing) { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Migrations were not applied yet, please apply them first.") + return nil, cmdx.FailSilently(cmd) + } else if validationErr := new(jsonschema.ValidationError); errors.As(err, &validationErr) { + // the configx provider already printed the validation error + return nil, cmdx.FailSilently(cmd) + } else if err != nil { + return nil, err + } + + conn, err := reg.PopConnection(cmd.Context()) + if err != nil { + return nil, err + } + if err := popx.VerifyDialect(cmd.Context(), conn); err != nil { + return nil, err + } + + return reg, nil +} diff --git a/cmd/server/serve.go b/cmd/server/serve.go new file mode 100644 index 0000000..a7a833a --- /dev/null +++ b/cmd/server/serve.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package server + +import ( + "github.com/spf13/cobra" + + "github.com/ory/keto/ketoctx" +) + +// serveCmd represents the serve command +func newServe(opts []ketoctx.Option) *cobra.Command { + cmd := &cobra.Command{ + Use: "serve", + Short: "Starts the server and serves the HTTP REST and gRPC APIs", + Long: `This command opens the network ports and listens to HTTP and gRPC API requests. + +## Configuration + +ORY Keto can be configured using environment variables as well as a configuration file. For more information +on configuration options, open the configuration documentation: + +>> https://www.ory.com/keto/docs/reference/configuration <<`, + RunE: func(cmd *cobra.Command, _ []string) error { + reg, err := NewRegistry(cmd, opts) + if err != nil { + return err + } + + return reg.ServeAllSQA(cmd) + }, + } + + cmd.Flags().Bool("sqa-opt-out", false, "Disable anonymized telemetry reports - for more information please visit https://www.ory.com/docs/ecosystem/sqa") + + return cmd +} + +func RegisterCommandsRecursive(parent *cobra.Command, opts []ketoctx.Option) { + parent.AddCommand(newServe(opts)) +} diff --git a/cmd/status/root.go b/cmd/status/root.go new file mode 100644 index 0000000..55e58d4 --- /dev/null +++ b/cmd/status/root.go @@ -0,0 +1,94 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/ory/x/cmdx" + "github.com/ory/x/stringsx" + "github.com/spf13/cobra" + "google.golang.org/grpc" + grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1" + + cliclient "github.com/ory/keto/cmd/client" +) + +const ( + FlagEndpoint = "endpoint" +) + +func newStatusCmd() *cobra.Command { + var endpoint string + + cmd := &cobra.Command{ + Use: "status", + Short: "Get the status of the upstream Keto instance", + Long: "Get a status report about the upstream Keto instance. Can also block until the service is healthy.", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + block, err := cmd.Flags().GetBool(cliclient.FlagBlock) + if err != nil { + return err + } + + var connect func(*cobra.Command) (*grpc.ClientConn, error) + + switch endpoints := stringsx.SwitchExact(endpoint); { + case endpoints.AddCase("read"): + connect = cliclient.GetReadConn + case endpoints.AddCase("write"): + connect = cliclient.GetWriteConn + default: + return endpoints.ToUnknownCaseErr() + } + + loudPrinter := cmdx.NewLoudOutPrinter(cmd) + + conn, err := connect(cmd) + for block && err != nil { + if !errors.Is(err, context.DeadlineExceeded) { + return err + } + _, _ = loudPrinter.Println("Context deadline exceeded, going to retry.") + conn, err = connect(cmd) + } + + if err != nil { + _, _ = fmt.Fprint(cmd.ErrOrStderr(), err.Error()) + return cmdx.FailSilently(cmd) + } + + c := grpcHealthV1.NewHealthClient(conn) + + for { + status, err := c.Check(cmd.Context(), &grpcHealthV1.HealthCheckRequest{}) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to get a check response: %+v\n", err) + return cmdx.FailSilently(cmd) + } + if !block || status.GetStatus() == grpcHealthV1.HealthCheckResponse_SERVING { + _, _ = fmt.Fprintln(cmd.OutOrStdout(), status.GetStatus().String()) + return nil + } + _, _ = loudPrinter.Printf("Got the status %s, waiting until %s.\n", status.GetStatus(), grpcHealthV1.HealthCheckResponse_SERVING) + time.Sleep(500 * time.Millisecond) + } + }, + } + + cliclient.RegisterRemoteURLFlags(cmd.Flags()) + cmdx.RegisterNoiseFlags(cmd.Flags()) + + cmd.Flags().StringVar(&endpoint, FlagEndpoint, "read", "which endpoint to use; one of {read, write}") + + return cmd +} + +func RegisterCommandRecursive(parent *cobra.Command) { + parent.AddCommand(newStatusCmd()) +} diff --git a/cmd/status/root_test.go b/cmd/status/root_test.go new file mode 100644 index 0000000..5aaf961 --- /dev/null +++ b/cmd/status/root_test.go @@ -0,0 +1,153 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package status + +import ( + "bytes" + "context" + "errors" + "net/http/httptest" + "strings" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1" + + "github.com/ory/x/cmdx" + + "github.com/ory/keto/cmd/client" + "github.com/ory/keto/cmd/client/clienttest" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/namespace" +) + +func TestStatusCmd(t *testing.T) { + ts := clienttest.NewTestServer(t, []*namespace.Namespace{{Name: t.Name()}}, newStatusCmd) + + for _, serverType := range []clienttest.ServerType{clienttest.ReadServer, clienttest.WriteServer} { + t.Run("server_type="+string(serverType), func(t *testing.T) { + ts.Cmd.PersistentArgs = append(ts.Cmd.PersistentArgs, "--"+cmdx.FlagQuiet, "--"+FlagEndpoint, string(serverType)) + + t.Run("case=timeout,noblock", func(t *testing.T) { + ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(-time.Second)) + defer cancel() + + stdErr := cmdx.ExecExpectedErrCtx(ctx, t, newStatusCmd(), "--"+FlagEndpoint, string(serverType)) + assert.Contains(t, stdErr, "context deadline exceeded") + }) + + t.Run("case=noblock", func(t *testing.T) { + stdOut := ts.Cmd.ExecNoErr(t) + assert.Equal(t, grpcHealthV1.HealthCheckResponse_SERVING.String()+"\n", stdOut) + }) + + t.Run("case=block", func(t *testing.T) { + ctx := context.WithValue(t.Context(), client.ContextKeyTimeout, 100*time.Millisecond) + + s := httptest.NewUnstartedServer(ts.NewHandler(t, serverType)) + s.EnableHTTP2 = true + + startServe := make(chan struct{}) + + serveErr := &errgroup.Group{} + serveErr.Go(func() error { + // wait until we get the signal to start + <-startServe + s.StartTLS() + return nil + }) + + var stdIn, stdErr bytes.Buffer + stdOut := cmdx.CallbackWriter{ + Callbacks: map[string]func([]byte) error{ + // once we get the first retry message, we want to start serving + "Context deadline exceeded, going to retry.": func([]byte) error { + // select ensures we only call this if the chan is not already closed + select { + case <-startServe: + default: + close(startServe) + } + return nil + }, + }, + } + + require.NoError(t, + cmdx.ExecBackgroundCtx(ctx, newStatusCmd(), &stdIn, &stdOut, &stdErr, + "--"+FlagEndpoint, string(serverType), + "--"+serverType.FlagName(), s.Listener.Addr().String(), + "--insecure-skip-hostname-verification=true", + "--"+client.FlagBlock, + ).Wait(), + ) + + fullStdOut := stdOut.String() + assert.True(t, strings.HasSuffix(fullStdOut, "\n"+grpcHealthV1.HealthCheckResponse_SERVING.String()+"\n"), fullStdOut) + + require.NoError(t, serveErr.Wait()) + }) + }) + } +} + +func authInterceptor(header, validValue string) connect.UnaryInterceptorFunc { + return func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + val := req.Header().Get(header) + if val == "" { + return nil, errors.New("not authorized, no header values") + } + if val != validValue { + return nil, errors.New("not authorized, incorrect value") + } + return next(ctx, req) + } + } +} + +func TestAuthorizedRequest(t *testing.T) { + ts := clienttest.NewTestServer( + t, []*namespace.Namespace{{Name: t.Name()}}, newStatusCmd, + driver.WithHandlerOptions(connect.WithInterceptors(authInterceptor("authorization", "Bearer secret"))), + ) + + t.Run("case=not authorized", func(t *testing.T) { + out := ts.Cmd.ExecExpectedErr(t) + assert.Contains(t, out, "not authorized") + }) + + t.Run("case=authorized", func(t *testing.T) { + t.Setenv("KETO_BEARER_TOKEN", "secret") + out := ts.Cmd.ExecNoErr(t) + assert.Contains(t, out, "SERVING") + }) +} + +func TestAuthorityRequest(t *testing.T) { + ts := clienttest.NewTestServer( + t, []*namespace.Namespace{{Name: t.Name()}}, newStatusCmd, + driver.WithHandlerOptions(connect.WithInterceptors(authInterceptor("Host", "example.com"))), + ) + + t.Run("case=no authority", func(t *testing.T) { + out := ts.Cmd.ExecExpectedErr(t) + assert.Contains(t, out, "not authorized") + }) + + t.Run("case=env authority", func(t *testing.T) { + t.Setenv("KETO_AUTHORITY", "example.com") + out := ts.Cmd.ExecNoErr(t) + assert.Contains(t, out, "SERVING") + }) + + t.Run("case=flag authority", func(t *testing.T) { + out := ts.Cmd.ExecNoErr(t, "--"+client.FlagAuthority, "example.com") + assert.Contains(t, out, "SERVING") + }) +} diff --git a/config/keto.yml b/config/keto.yml new file mode 100644 index 0000000..45a83ed --- /dev/null +++ b/config/keto.yml @@ -0,0 +1,18 @@ +version: v0.8.0-alpha.2 + +log: + level: debug + +namespaces: + - id: 0 + name: videos + +serve: + read: + host: 0.0.0.0 + port: 4466 + write: + host: 0.0.0.0 + port: 4467 + +dsn: memory diff --git a/contrib/cat-videos-example/docker-compose.yml b/contrib/cat-videos-example/docker-compose.yml new file mode 100644 index 0000000..e155395 --- /dev/null +++ b/contrib/cat-videos-example/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.2" + +services: + keto: + image: oryd/keto:v25.4.0 + working_dir: /home/ory + ports: + - "4466:4466" + - "4467:4467" + command: serve -c keto.yml + restart: on-failure + volumes: + - type: bind + source: . + target: /home/ory + + keto-init: + image: oryd/keto:v25.4.0 + working_dir: /home/ory + environment: + - KETO_WRITE_REMOTE=keto:4467 + volumes: + - type: bind + source: . + target: /home/ory + command: + relation-tuple create + ./relation-tuples --insecure-disable-transport-security + restart: on-failure diff --git a/contrib/cat-videos-example/keto.yml b/contrib/cat-videos-example/keto.yml new file mode 100644 index 0000000..2643d1b --- /dev/null +++ b/contrib/cat-videos-example/keto.yml @@ -0,0 +1,17 @@ +version: v0.6.0-alpha.1 + +log: + level: debug + +namespaces: + location: file://./namespaces.ts + +dsn: memory + +serve: + read: + host: 0.0.0.0 + port: 4466 + write: + host: 0.0.0.0 + port: 4467 diff --git a/contrib/cat-videos-example/namespaces.ts b/contrib/cat-videos-example/namespaces.ts new file mode 100644 index 0000000..72678f0 --- /dev/null +++ b/contrib/cat-videos-example/namespaces.ts @@ -0,0 +1,13 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Context, Namespace, SubjectSet } from "@ory/keto-namespace-types" + +class User implements Namespace {} + +class Video implements Namespace { + related: { + view: (User | SubjectSet)[] + owner: User[] + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats1_owner.json b/contrib/cat-videos-example/relation-tuples/cats1_owner.json new file mode 100644 index 0000000..e8435e7 --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats1_owner.json @@ -0,0 +1,10 @@ +{ + "namespace": "Video", + "object": "/cats/1.mp4", + "relation": "owner", + "subject_set": { + "namespace": "Video", + "object": "/cats", + "relation": "owner" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json b/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json new file mode 100644 index 0000000..d08a2b5 --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats1_view_owner.json @@ -0,0 +1,10 @@ +{ + "namespace": "Video", + "object": "/cats/1.mp4", + "relation": "view", + "subject_set": { + "namespace": "Video", + "object": "/cats/1.mp4", + "relation": "owner" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats1_view_public.json b/contrib/cat-videos-example/relation-tuples/cats1_view_public.json new file mode 100644 index 0000000..13d29cc --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats1_view_public.json @@ -0,0 +1,9 @@ +{ + "namespace": "Video", + "object": "/cats/1.mp4", + "relation": "view", + "subject_set": { + "namespace": "User", + "object": "*" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats2_owner.json b/contrib/cat-videos-example/relation-tuples/cats2_owner.json new file mode 100644 index 0000000..bd8ef26 --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats2_owner.json @@ -0,0 +1,10 @@ +{ + "namespace": "Video", + "object": "/cats/2.mp4", + "relation": "owner", + "subject_set": { + "namespace": "Video", + "object": "/cats", + "relation": "owner" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats2_view.json b/contrib/cat-videos-example/relation-tuples/cats2_view.json new file mode 100644 index 0000000..81393cf --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats2_view.json @@ -0,0 +1,10 @@ +{ + "namespace": "Video", + "object": "/cats/2.mp4", + "relation": "view", + "subject_set": { + "namespace": "Video", + "object": "/cats/2.mp4", + "relation": "owner" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats_owner.json b/contrib/cat-videos-example/relation-tuples/cats_owner.json new file mode 100644 index 0000000..389483e --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats_owner.json @@ -0,0 +1,9 @@ +{ + "namespace": "Video", + "object": "/cats", + "relation": "owner", + "subject_set": { + "namespace": "User", + "object": "Alice" + } +} diff --git a/contrib/cat-videos-example/relation-tuples/cats_view.json b/contrib/cat-videos-example/relation-tuples/cats_view.json new file mode 100644 index 0000000..ed7d67f --- /dev/null +++ b/contrib/cat-videos-example/relation-tuples/cats_view.json @@ -0,0 +1,10 @@ +{ + "namespace": "Video", + "object": "/cats", + "relation": "view", + "subject_set": { + "namespace": "Video", + "object": "/cats", + "relation": "owner" + } +} diff --git a/contrib/docs-code-samples/.gitignore b/contrib/docs-code-samples/.gitignore new file mode 100644 index 0000000..397b4a7 --- /dev/null +++ b/contrib/docs-code-samples/.gitignore @@ -0,0 +1 @@ +*.log diff --git a/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/cli.sh b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/cli.sh new file mode 100755 index 0000000..1346027 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/cli.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +echo '// ownership +directories:/photos#owner@maureen +files:/photos/beach.jpg#owner@maureen +files:/photos/mountains.jpg#owner@laura + +// maureen granted access to /photos to laura +directories:/photos#access@laura + +// the following tuples are defined implicitly through subject set rewrites (not supported yet) +directories:/photos#access@(directories:/photos#owner) +files:/photos/beach.jpg#access@(files:/photos/beach.jpg#owner) +files:/photos/beach.jpg#access@(directories:/photos#access) +files:/photos/mountains.jpg#access@(files:/photos/mountains.jpg#owner) +files:/photos/mountains.jpg#access@(directories:/photos#access)' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - --insecure-disable-transport-security >/dev/null \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/curl.sh b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/curl.sh new file mode 100755 index 0000000..e280fae --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/curl.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +echo '// ownership +directories:/photos#owner@maureen +files:/photos/beach.jpg#owner@maureen +files:/photos/mountains.jpg#owner@laura + +// maureen granted access to /photos to laura +directories:/photos#access@laura + +// the following tuples are defined implicitly through subject set rewrites (not supported yet) +directories:/photos#access@(directories:/photos#owner) +files:/photos/beach.jpg#access@(files:/photos/beach.jpg#owner) +files:/photos/beach.jpg#access@(directories:/photos#access) +files:/photos/mountains.jpg#access@(files:/photos/mountains.jpg#owner) +files:/photos/mountains.jpg#access@(directories:/photos#access)' | \ + keto relation-tuple parse -f - --format json | \ + jq "[ .[] | { relation_tuple: . , action: \"insert\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples + +echo "Successfully created tuples" diff --git a/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/expected_output.txt b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/expected_output.txt new file mode 100644 index 0000000..6306203 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuples diff --git a/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/index.js b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/index.js new file mode 100644 index 0000000..41becb2 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/index.js @@ -0,0 +1,92 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +const writeRequest = new write.TransactRelationTuplesRequest() + +const insert = (tuple) => { + const tupleDelta = new write.RelationTupleDelta() + tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + tupleDelta.setRelationTuple(tuple) + + writeRequest.addRelationTupleDeltas(tupleDelta) +} + +const addSimpleTuple = (namespace, object, relation, user) => { + const relationTuple = new relationTuples.RelationTuple() + relationTuple.setNamespace(namespace) + relationTuple.setObject(object) + relationTuple.setRelation(relation) + + const sub = new relationTuples.Subject() + sub.setId(user) + relationTuple.setSubject(sub) + + insert(relationTuple) +} + +// ownership +addSimpleTuple("directories", "/photos", "owner", "maureen") +addSimpleTuple("files", "/photos/beach.jpg", "owner", "maureen") +addSimpleTuple("files", "/photos/mountains.jpg", "owner", "laura") +// granted access +addSimpleTuple("directories", "/photos", "access", "laura") + +// should be subject set rewrite +// owners have access +;[ + ["files", "/photos/beach.jpg"], + ["files", "/photos/mountains.jpg"], + ["directories", "/photos"], +].forEach(([namespace, object]) => { + const relationTuple = new relationTuples.RelationTuple() + relationTuple.setNamespace(namespace) + relationTuple.setObject(object) + relationTuple.setRelation("access") + + const subjectSet = new relationTuples.SubjectSet() + subjectSet.setNamespace(namespace) + subjectSet.setObject(object) + subjectSet.setRelation("owner") + + const sub = new relationTuples.Subject() + sub.setSet(subjectSet) + relationTuple.setSubject(sub) + + insert(relationTuple) +}) + +// should be subject set rewrite +// access on parent means access on child +;["/photos/beach.jpg", "/photos/mountains.jpg"].forEach((file) => { + const relationTuple = new relationTuples.RelationTuple() + relationTuple.setNamespace("files") + relationTuple.setObject(file) + relationTuple.setRelation("access") + + const subjectSet = new relationTuples.SubjectSet() + subjectSet.setNamespace("directories") + subjectSet.setObject("/photos") + subjectSet.setRelation("access") + + const sub = new relationTuples.Subject() + sub.setSet(subjectSet) + relationTuple.setSubject(sub) + + insert(relationTuple) +}) + +writeClient.transactRelationTuples(writeRequest, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuples") + } +}) diff --git a/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/main.go b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/main.go new file mode 100644 index 0000000..8207267 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/00-create-tuples/main.go @@ -0,0 +1,95 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + client := rts.NewWriteServiceClient(conn) + + //directories:/photos#owner@maureen + //files:/photos/beach.jpg#owner@maureen + //files:/photos/mountains.jpg#owner@laura + //directories:/photos#access@laura + //directories:/photos#access@(directories:/photos#owner) + //files:/photos/beach.jpg#access@(files:/photos/beach.jpg#owner) + //files:/photos/beach.jpg#access@(directories:/photos#access) + //files:/photos/mountains.jpg#access@(files:/photos/mountains.jpg#owner) + //files:/photos/mountains.jpg#access@(directories:/photos#access) + + tuples := []*rts.RelationTuple{ + // ownership + { + Namespace: "directories", + Object: "/photos", + Relation: "owner", + Subject: rts.NewSubjectID("maureen"), + }, + { + Namespace: "files", + Object: "/photos/beach.jpg", + Relation: "owner", + Subject: rts.NewSubjectID("maureen"), + }, + { + Namespace: "files", + Object: "/photos/mountains.jpg", + Relation: "owner", + Subject: rts.NewSubjectID("laura"), + }, + // granted access + { + Namespace: "directories", + Object: "/photos", + Relation: "access", + Subject: rts.NewSubjectID("laura"), + }, + } + // should be subject set rewrite + // owners have access + for _, o := range []struct{ n, o string }{ + {"files", "/photos/beach.jpg"}, + {"files", "/photos/mountains.jpg"}, + {"directories", "/photos"}, + } { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: o.n, + Object: o.o, + Relation: "access", + Subject: rts.NewSubjectSet(o.n, o.o, "owner"), + }) + } + // should be subject set rewrite + // access on parent means access on child + for _, obj := range []string{"/photos/beach.jpg", "/photos/mountains.jpg"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "files", + Object: obj, + Relation: "access", + Subject: rts.NewSubjectSet("directories", "/photos", "access"), + }) + } + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: rts.RelationTupleToDeltas(tuples, rts.RelationTupleDelta_ACTION_INSERT), + }) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + fmt.Println("Successfully created tuples") +} diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/cli.sh b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/cli.sh new file mode 100755 index 0000000..1bdda02 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/cli.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto expand access files:/photos/beach.jpg --format json-pretty --max-depth 3 --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/curl.sh b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/curl.sh new file mode 100755 index 0000000..c9e2ebb --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/curl.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=files" \ + --data-urlencode "relation=access" \ + --data-urlencode "object=/photos/beach.jpg" \ + --data-urlencode "max-depth=3" \ + http://127.0.0.1:4466/relation-tuples/expand | \ + jq diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.json b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.json new file mode 100644 index 0000000..8c61c2e --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/expected_output.json @@ -0,0 +1,76 @@ +{ + "children": [ + { + "children": [ + { + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_id": "maureen" + }, + "type": "leaf" + } + ], + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_set": { + "namespace": "files", + "object": "/photos/beach.jpg", + "relation": "owner" + } + }, + "type": "union" + }, + { + "children": [ + { + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_id": "laura" + }, + "type": "leaf" + }, + { + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_set": { + "namespace": "directories", + "object": "/photos", + "relation": "owner" + } + }, + "type": "leaf" + } + ], + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_set": { + "namespace": "directories", + "object": "/photos", + "relation": "access" + } + }, + "type": "union" + } + ], + "tuple": { + "namespace": "", + "object": "", + "relation": "", + "subject_set": { + "namespace": "files", + "object": "/photos/beach.jpg", + "relation": "access" + } + }, + "type": "union" +} diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js new file mode 100644 index 0000000..9c225a9 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/index.js @@ -0,0 +1,71 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, expand, expandService } from "@ory/keto-grpc-client" + +const expandClient = new expandService.ExpandServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const subjectSet = new relationTuples.SubjectSet() +subjectSet.setNamespace("files") +subjectSet.setRelation("access") +subjectSet.setObject("/photos/beach.jpg") + +const sub = new relationTuples.Subject() +sub.setSet(subjectSet) + +const expandRequest = new expand.ExpandRequest() +expandRequest.setSubject(sub) +expandRequest.setMaxDepth(3) + +// helper to get a nice result +const subjectJSON = (subject) => { + if (subject.hasId()) { + return { subject_id: subject.getId() } + } + const set = subject.getSet() + return { + subject_set: { + namespace: set.getNamespace(), + object: set.getObject(), + relation: set.getRelation(), + }, + } +} + +// helper to get a nice result +const prettyTree = (tree) => { + const [nodeType, tuple, children] = [ + tree.getNodeType(), + { + tuple: { + namespace: "", + object: "", + relation: "", + ...subjectJSON(tree.getSubject()), + }, + }, + tree.getChildrenList(), + ] + switch (nodeType) { + case expand.NodeType.NODE_TYPE_LEAF: + return { type: "leaf", ...tuple } + case expand.NodeType.NODE_TYPE_UNION: { + const mapped = children.map(prettyTree) + return mapped.length > 0 + ? { type: "union", children: mapped, ...tuple } + : { type: "union", ...tuple } + } + } +} + +expandClient.expand(expandRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log(JSON.stringify(prettyTree(resp.getTree()), null, 2)) + } +}) diff --git a/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/main.go b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/main.go new file mode 100644 index 0000000..aa30df9 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/01-expand-beach/main.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "encoding/json" + "os" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewExpandServiceClient(conn) + + res, err := client.Expand(context.Background(), &rts.ExpandRequest{ + Subject: rts.NewSubjectSet("files", "/photos/beach.jpg", "access"), + MaxDepth: 3, + }) + if err != nil { + panic(err) + } + + tree := ketoapi.TreeFromProto[*ketoapi.RelationTuple](res.Tree) + + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(tree); err != nil { + panic(err.Error()) + } +} diff --git a/contrib/docs-code-samples/expand-api-display-access/99-cleanup/cli.sh b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/cli.sh new file mode 100755 index 0000000..23bb179 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/cli.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +keto relation-tuple get --namespace files --format json --insecure-disable-transport-security | \ + jq ".relation_tuples" | \ + keto relation-tuple delete --insecure-disable-transport-security -f - -q > /dev/null + +keto relation-tuple get --namespace directories --format json --insecure-disable-transport-security | \ + jq ".relation_tuples" | \ + keto relation-tuple delete --insecure-disable-transport-security -f - -q > /dev/null diff --git a/contrib/docs-code-samples/expand-api-display-access/99-cleanup/curl.sh b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/curl.sh new file mode 100755 index 0000000..f45a07c --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/curl.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=files" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[ .relation_tuples[] | { relation_tuple: . , action: \"delete\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=directories" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[ .relation_tuples[] | { relation_tuple: . , action: \"delete\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples diff --git a/contrib/docs-code-samples/expand-api-display-access/99-cleanup/expected_output.txt b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/expected_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/contrib/docs-code-samples/expand-api-display-access/99-cleanup/index.js b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/index.js new file mode 100644 index 0000000..da3452d --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/index.js @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const purgeNamespace = (namespace) => { + const query = new relationTuples.RelationQuery() + query.setNamespace(namespace) + const request = new write.DeleteRelationTuplesRequest() + request.setRelationQuery(query) + new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), + ).deleteRelationTuples(request, (err) => { + if (err) { + console.error(err) + } + }) +} + +purgeNamespace("files") +purgeNamespace("directories") diff --git a/contrib/docs-code-samples/expand-api-display-access/99-cleanup/main.go b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/main.go new file mode 100644 index 0000000..bbdf08e --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/99-cleanup/main.go @@ -0,0 +1,62 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build docscodesamples +// +build docscodesamples + +package main + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func purgeNamespace(nspace string) { + rc, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer rc.Close() + + rClient := rts.NewReadServiceClient(rc) + resp, err := rClient.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + Query: &rts.ListRelationTuplesRequest_Query{ + Namespace: nspace, + }, + }) + if err != nil { + panic(err) + } + + wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer wc.Close() + + deltas := make([]*rts.RelationTupleDelta, len(resp.RelationTuples)) + for i, rt := range resp.RelationTuples { + deltas[i] = &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_DELETE, + RelationTuple: proto.Clone(rt).(*rts.RelationTuple), + } + } + + wClient := rts.NewWriteServiceClient(wc) + _, err = wClient.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + if err != nil { + panic(err) + } +} + +func main() { + purgeNamespace("directories") + purgeNamespace("files") +} diff --git a/contrib/docs-code-samples/expand-api-display-access/keto.yml b/contrib/docs-code-samples/expand-api-display-access/keto.yml new file mode 100644 index 0000000..5b8f586 --- /dev/null +++ b/contrib/docs-code-samples/expand-api-display-access/keto.yml @@ -0,0 +1,9 @@ +dsn: memory +log: + level: trace + +namespaces: + - name: files + id: 1 + - name: directories + id: 2 diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/cli.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/cli.sh new file mode 100755 index 0000000..b82db71 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/cli.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +echo 'Chat:memes#member@User:PM +Chat:memes#member@User:Vincent +Chat:memes#member@User:Julia + +Chat:cars#member@User:PM +Chat:cars#member@User:Julia + +Chat:coffee-break#member@User:PM +Chat:coffee-break#member@User:Vincent +Chat:coffee-break#member@User:Julia +Chat:coffee-break#member@User:Patrik' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/curl.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/curl.sh new file mode 100755 index 0000000..3c8d413 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/curl.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +echo 'Chat:memes#member@User:PM +Chat:memes#member@User:Vincent +Chat:memes#member@User:Julia + +Chat:cars#member@User:PM +Chat:cars#member@User:Julia + +Chat:coffee-break#member@User:PM +Chat:coffee-break#member@User:Vincent +Chat:coffee-break#member@User:Julia +Chat:coffee-break#member@User:Patrik' | \ + keto relation-tuple parse -f - --format json | \ + jq "[ .[] | { relation_tuple: . , action: \"insert\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples + +echo "Successfully created tuples" diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/expected_output.txt new file mode 100644 index 0000000..6306203 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuples diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/index.js b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/index.js new file mode 100644 index 0000000..53f648c --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/index.js @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +const writeRequest = new write.TransactRelationTuplesRequest() + +const addToChat = (chatName) => (user) => { + const relationTuple = new relationTuples.RelationTuple() + relationTuple.setNamespace("Chat") + relationTuple.setObject(chatName) + relationTuple.setRelation("member") + + const subSet = new relationTuples.SubjectSet() + subSet.setNamespace("User") + subSet.setObject(user) + + const sub = new relationTuples.Subject() + sub.setSet(subSet) + relationTuple.setSubject(sub) + + const tupleDelta = new write.RelationTupleDelta() + tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + tupleDelta.setRelationTuple(relationTuple) + + writeRequest.addRelationTupleDeltas(tupleDelta) +} + +;["PM", "Vincent", "Julia"].forEach(addToChat("memes")) +;["PM", "Julia"].forEach(addToChat("cars")) +;["PM", "Vincent", "Julia", "Patrik"].forEach(addToChat("coffee-break")) + +writeClient.transactRelationTuples(writeRequest, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuples") + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/main.go b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/main.go new file mode 100644 index 0000000..a2f9718 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/00-create-tuples/main.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + client := rts.NewWriteServiceClient(conn) + + var tuples []*rts.RelationTuple + // memes + for _, user := range []string{"PM", "Vincent", "Julia"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "Chat", + Object: "memes", + Relation: "member", + Subject: rts.NewSubjectSet("User", user, ""), + }) + } + // cars + for _, user := range []string{"PM", "Julia"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "Chat", + Object: "cars", + Relation: "member", + Subject: rts.NewSubjectSet("User", user, ""), + }) + } + // coffee-break + for _, user := range []string{"PM", "Vincent", "Julia", "Patrik"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "Chat", + Object: "coffee-break", + Relation: "member", + Subject: rts.NewSubjectSet("User", user, ""), + }) + } + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: rts.RelationTupleToDeltas(tuples, rts.RelationTupleDelta_ACTION_INSERT), + }) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + fmt.Println("Successfully created tuples") +} diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/cli.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/cli.sh new file mode 100755 index 0000000..0c5ee3b --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/cli.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto relation-tuple get --namespace Chat --relation member --subject-set User:PM --format json --insecure-disable-transport-security | \ + jq ".relation_tuples[] | .object" -r | sort diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/curl.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/curl.sh new file mode 100755 index 0000000..461ec7d --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/curl.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=Chat" \ + --data-urlencode "relation=member" \ + --data-urlencode "subject_set.namespace=User" \ + --data-urlencode "subject_set.object=PM" \ + --data-urlencode "subject_set.relation=" \ + http://127.0.0.1:4466/relation-tuples | \ + jq ".relation_tuples[] | .object" -r | sort diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/expected_output.txt new file mode 100644 index 0000000..7f76522 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/expected_output.txt @@ -0,0 +1,3 @@ +cars +coffee-break +memes diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/index.js b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/index.js new file mode 100644 index 0000000..a30ddd4 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/index.js @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const readRequest = new read.ListRelationTuplesRequest() +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("Chat") +query.setRelation("member") + +const subSet = new relationTuples.SubjectSet() +subSet.setNamespace("User") +subSet.setObject("PM") + +const sub = new relationTuples.Subject() +sub.setSet(subSet) +query.setSubject(sub) + +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log( + resp + .getRelationTuplesList() + .map((tuple) => tuple.getObject()) + .sort((a, b) => (a < b ? -1 : 1)) + .join("\n"), + ) + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/main.go b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/main.go new file mode 100644 index 0000000..e769449 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/01-list-PM/main.go @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "sort" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err.Error()) + } + + client := rts.NewReadServiceClient(conn) + + res, err := client.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + RelationQuery: &rts.RelationQuery{ + Namespace: new("Chat"), + Relation: new("member"), + Subject: rts.NewSubjectSet("User", "PM", ""), + }, + }) + if err != nil { + panic(err.Error()) + } + + objects := make([]string, len(res.RelationTuples)) + for i, rt := range res.RelationTuples { + objects[i] = rt.Object + } + sort.Strings(objects) + for _, o := range objects { + fmt.Println(o) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/cli.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/cli.sh new file mode 100755 index 0000000..9e997cf --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/cli.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto relation-tuple get --namespace Chat --object coffee-break --relation member --format json --insecure-disable-transport-security | \ + jq "[.relation_tuples[] | .subject_set.object] | sort | .[]" -r diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/curl.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/curl.sh new file mode 100755 index 0000000..993ae26 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/curl.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=Chat" \ + --data-urlencode "object=coffee-break" \ + --data-urlencode "relation=member" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[.relation_tuples[] | .subject_set.object] | sort | .[]" -r diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/expected_output.txt new file mode 100644 index 0000000..e196c52 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/expected_output.txt @@ -0,0 +1,4 @@ +Julia +PM +Patrik +Vincent diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/index.js b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/index.js new file mode 100644 index 0000000..724e272 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/index.js @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const readRequest = new read.ListRelationTuplesRequest() +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("Chat") +query.setObject("coffee-break") +query.setRelation("member") + +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log( + resp + .getRelationTuplesList() + .map((tuple) => tuple.getSubject().getSet().getObject()) + .sort((a, b) => (a < b ? -1 : 1)) + .join("\n"), + ) + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/main.go b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/main.go new file mode 100644 index 0000000..d692a4f --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/02-list-coffee-break/main.go @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "sort" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err.Error()) + } + + client := rts.NewReadServiceClient(conn) + + res, err := client.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + RelationQuery: &rts.RelationQuery{ + Namespace: new("Chat"), + Object: new("coffee-break"), + Relation: new("member"), + }, + }) + if err != nil { + panic(err.Error()) + } + + ids := make([]string, len(res.RelationTuples)) + for i, rt := range res.RelationTuples { + ids[i] = rt.Subject.Ref.(*rts.Subject_Set).Set.Object + } + sort.Strings(ids) + for _, id := range ids { + fmt.Println(id) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/cli.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/cli.sh new file mode 100755 index 0000000..7883625 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/cli.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +keto relation-tuple get --namespace Chat --format json --insecure-disable-transport-security | \ + jq ".relation_tuples" | \ + keto relation-tuple delete -f - -q > /dev/null --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/curl.sh b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/curl.sh new file mode 100755 index 0000000..f099c00 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/curl.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=Chat" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[ .relation_tuples[] | { relation_tuple: . , action: \"delete\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/expected_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/index.js b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/index.js new file mode 100644 index 0000000..b7e7b1a --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/index.js @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { write, writeService, read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("Chat") + +const readRequest = new read.ListRelationTuplesRequest() +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (err, resp) => { + const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), + ) + + const writeRequest = new write.TransactRelationTuplesRequest() + + resp.getRelationTuplesList().forEach((tuple) => { + const tupleDelta = new write.RelationTupleDelta() + tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_DELETE) + tupleDelta.setRelationTuple(tuple) + writeRequest.addRelationTupleDeltas(tupleDelta) + }) + + writeClient.transactRelationTuples(writeRequest, (err) => { + if (err) { + console.log("Unexpected err", err) + } + }) +}) diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/main.go b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/main.go new file mode 100644 index 0000000..7b7e106 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/99-cleanup/main.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build docscodesamples +// +build docscodesamples + +package main + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + rc, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer rc.Close() + + rClient := rts.NewReadServiceClient(rc) + resp, err := rClient.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + Query: &rts.ListRelationTuplesRequest_Query{ + Namespace: "Chat", + }, + }) + if err != nil { + panic(err) + } + + wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer wc.Close() + + deltas := make([]*rts.RelationTupleDelta, len(resp.RelationTuples)) + for i, rt := range resp.RelationTuples { + deltas[i] = &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_DELETE, + RelationTuple: proto.Clone(rt).(*rts.RelationTuple), + } + } + + wClient := rts.NewWriteServiceClient(wc) + _, err = wClient.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + if err != nil { + panic(err) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/keto.yml b/contrib/docs-code-samples/list-api-display-objects-subjectset/keto.yml new file mode 100644 index 0000000..ea8b7f2 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/keto.yml @@ -0,0 +1,6 @@ +dsn: memory +log: + level: trace + +namespaces: + location: file://./list-api-display-objects-subjectset/namespaces.ts diff --git a/contrib/docs-code-samples/list-api-display-objects-subjectset/namespaces.ts b/contrib/docs-code-samples/list-api-display-objects-subjectset/namespaces.ts new file mode 100644 index 0000000..4382207 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects-subjectset/namespaces.ts @@ -0,0 +1,12 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Context, Namespace } from "@ory/keto-namespace-types" + +class User implements Namespace {} + +class Chat implements Namespace { + related: { + member: User[] + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/cli.sh b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/cli.sh new file mode 100755 index 0000000..f3f8c8b --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/cli.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +echo 'chats:memes#member@PM +chats:memes#member@Vincent +chats:memes#member@Julia + +chats:cars#member@PM +chats:cars#member@Julia + +chats:coffee-break#member@PM +chats:coffee-break#member@Vincent +chats:coffee-break#member@Julia +chats:coffee-break#member@Patrik' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/curl.sh b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/curl.sh new file mode 100755 index 0000000..21a1b44 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/curl.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +echo 'chats:memes#member@PM +chats:memes#member@Vincent +chats:memes#member@Julia + +chats:cars#member@PM +chats:cars#member@Julia + +chats:coffee-break#member@PM +chats:coffee-break#member@Vincent +chats:coffee-break#member@Julia +chats:coffee-break#member@Patrik' | \ + keto relation-tuple parse -f - --format json | \ + jq "[ .[] | { relation_tuple: . , action: \"insert\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples + +echo "Successfully created tuples" diff --git a/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/expected_output.txt new file mode 100644 index 0000000..6306203 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuples diff --git a/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/index.js b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/index.js new file mode 100644 index 0000000..81d46cb --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/index.js @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +const writeRequest = new write.TransactRelationTuplesRequest() + +const addToChat = (chatName) => (user) => { + const relationTuple = new relationTuples.RelationTuple() + relationTuple.setNamespace("chats") + relationTuple.setObject(chatName) + relationTuple.setRelation("member") + + const sub = new relationTuples.Subject() + sub.setId(user) + relationTuple.setSubject(sub) + + const tupleDelta = new write.RelationTupleDelta() + tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + tupleDelta.setRelationTuple(relationTuple) + + writeRequest.addRelationTupleDeltas(tupleDelta) +} + +;["PM", "Vincent", "Julia"].forEach(addToChat("memes")) +;["PM", "Julia"].forEach(addToChat("cars")) +;["PM", "Vincent", "Julia", "Patrik"].forEach(addToChat("coffee-break")) + +writeClient.transactRelationTuples(writeRequest, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuples") + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/main.go b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/main.go new file mode 100644 index 0000000..00b8e73 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/00-create-tuples/main.go @@ -0,0 +1,61 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + client := rts.NewWriteServiceClient(conn) + + var tuples []*rts.RelationTuple + // memes + for _, user := range []string{"PM", "Vincent", "Julia"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "chats", + Object: "memes", + Relation: "member", + Subject: rts.NewSubjectID(user), + }) + } + // cars + for _, user := range []string{"PM", "Julia"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "chats", + Object: "cars", + Relation: "member", + Subject: rts.NewSubjectID(user), + }) + } + // coffee-break + for _, user := range []string{"PM", "Vincent", "Julia", "Patrik"} { + tuples = append(tuples, &rts.RelationTuple{ + Namespace: "chats", + Object: "coffee-break", + Relation: "member", + Subject: rts.NewSubjectID(user), + }) + } + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: rts.RelationTupleToDeltas(tuples, rts.RelationTupleDelta_ACTION_INSERT), + }) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + fmt.Println("Successfully created tuples") +} diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh new file mode 100755 index 0000000..7365b83 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/cli.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto relation-tuple get --namespace chats --relation member --subject-id PM --format json --insecure-disable-transport-security | \ + jq ".relation_tuples[] | .object" -r | sort diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh new file mode 100755 index 0000000..e8a045c --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/curl.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=chats" \ + --data-urlencode "relation=member" \ + --data-urlencode "subject_id=PM" \ + http://127.0.0.1:4466/relation-tuples | \ + jq ".relation_tuples[] | .object" -r | sort diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/expected_output.txt new file mode 100644 index 0000000..7f76522 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/expected_output.txt @@ -0,0 +1,3 @@ +cars +coffee-break +memes diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/index.js b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/index.js new file mode 100644 index 0000000..e97a7e3 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/index.js @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const readRequest = new read.ListRelationTuplesRequest() +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("chats") +query.setRelation("member") + +const sub = new relationTuples.Subject() +sub.setId("PM") +query.setSubject(sub) + +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log( + resp + .getRelationTuplesList() + .map((tuple) => tuple.getObject()) + .sort((a, b) => (a < b ? -1 : 1)) + .join("\n"), + ) + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects/01-list-PM/main.go b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/main.go new file mode 100644 index 0000000..be565a2 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/01-list-PM/main.go @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "sort" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err.Error()) + } + + client := rts.NewReadServiceClient(conn) + + res, err := client.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + RelationQuery: &rts.RelationQuery{ + Namespace: new("chats"), + Relation: new("member"), + Subject: rts.NewSubjectID("PM"), + }, + }) + if err != nil { + panic(err.Error()) + } + + objects := make([]string, len(res.RelationTuples)) + for i, rt := range res.RelationTuples { + objects[i] = rt.Object + } + sort.Strings(objects) + for _, o := range objects { + fmt.Println(o) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh new file mode 100755 index 0000000..636fbbc --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/cli.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto relation-tuple get --namespace chats --object coffee-break --relation member --format json --insecure-disable-transport-security | \ + jq "[.relation_tuples[] | .subject_id] | sort | .[]" -r diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh new file mode 100755 index 0000000..de1e6ee --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/curl.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=chats" \ + --data-urlencode "object=coffee-break" \ + --data-urlencode "relation=member" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[.relation_tuples[] | .subject_id] | sort | .[]" -r diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/expected_output.txt new file mode 100644 index 0000000..e196c52 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/expected_output.txt @@ -0,0 +1,4 @@ +Julia +PM +Patrik +Vincent diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/index.js b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/index.js new file mode 100644 index 0000000..cec4cbf --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/index.js @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const readRequest = new read.ListRelationTuplesRequest() +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("chats") +query.setObject("coffee-break") +query.setRelation("member") + +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log( + resp + .getRelationTuplesList() + .map((tuple) => tuple.getSubject().getId()) + .sort((a, b) => (a < b ? -1 : 1)) + .join("\n"), + ) + } +}) diff --git a/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/main.go b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/main.go new file mode 100644 index 0000000..83306f2 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/02-list-coffee-break/main.go @@ -0,0 +1,44 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "sort" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err.Error()) + } + + client := rts.NewReadServiceClient(conn) + + res, err := client.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + RelationQuery: &rts.RelationQuery{ + Namespace: new("chats"), + Object: new("coffee-break"), + Relation: new("member"), + }, + }) + if err != nil { + panic(err.Error()) + } + + ids := make([]string, len(res.RelationTuples)) + for i, rt := range res.RelationTuples { + ids[i] = rt.Subject.Ref.(*rts.Subject_Id).Id + } + sort.Strings(ids) + for _, id := range ids { + fmt.Println(id) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects/99-cleanup/cli.sh b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/cli.sh new file mode 100755 index 0000000..b01e3be --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/cli.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +keto relation-tuple get --namespace chats --format json --insecure-disable-transport-security | \ + jq ".relation_tuples" | \ + keto relation-tuple delete -f - -q > /dev/null --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/list-api-display-objects/99-cleanup/curl.sh b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/curl.sh new file mode 100755 index 0000000..089e7d7 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/curl.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=chats" \ + http://127.0.0.1:4466/relation-tuples | \ + jq "[ .relation_tuples[] | { relation_tuple: . , action: \"delete\" } ]" -c | \ + curl -X PATCH --silent --fail \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data @- \ + http://127.0.0.1:4467/admin/relation-tuples diff --git a/contrib/docs-code-samples/list-api-display-objects/99-cleanup/expected_output.txt b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/expected_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/contrib/docs-code-samples/list-api-display-objects/99-cleanup/index.js b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/index.js new file mode 100644 index 0000000..91e1c69 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/index.js @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { write, writeService, read, readService } from "@ory/keto-grpc-client" + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const query = new read.ListRelationTuplesRequest.Query() +query.setNamespace("chats") + +const readRequest = new read.ListRelationTuplesRequest() +readRequest.setQuery(query) + +readClient.listRelationTuples(readRequest, (err, resp) => { + const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), + ) + + const writeRequest = new write.TransactRelationTuplesRequest() + + resp.getRelationTuplesList().forEach((tuple) => { + const tupleDelta = new write.RelationTupleDelta() + tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_DELETE) + tupleDelta.setRelationTuple(tuple) + writeRequest.addRelationTupleDeltas(tupleDelta) + }) + + writeClient.transactRelationTuples(writeRequest, (err) => { + if (err) { + console.log("Unexpected err", err) + } + }) +}) diff --git a/contrib/docs-code-samples/list-api-display-objects/99-cleanup/main.go b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/main.go new file mode 100644 index 0000000..ea844e1 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/99-cleanup/main.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build docscodesamples +// +build docscodesamples + +package main + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/protobuf/proto" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + rc, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer rc.Close() + + rClient := rts.NewReadServiceClient(rc) + resp, err := rClient.ListRelationTuples(context.Background(), &rts.ListRelationTuplesRequest{ + Query: &rts.ListRelationTuplesRequest_Query{ + Namespace: "chats", + }, + }) + if err != nil { + panic(err) + } + + wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer wc.Close() + + deltas := make([]*rts.RelationTupleDelta, len(resp.RelationTuples)) + for i, rt := range resp.RelationTuples { + deltas[i] = &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_DELETE, + RelationTuple: proto.Clone(rt).(*rts.RelationTuple), + } + } + + wClient := rts.NewWriteServiceClient(wc) + _, err = wClient.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + if err != nil { + panic(err) + } +} diff --git a/contrib/docs-code-samples/list-api-display-objects/keto.yml b/contrib/docs-code-samples/list-api-display-objects/keto.yml new file mode 100644 index 0000000..9f0a233 --- /dev/null +++ b/contrib/docs-code-samples/list-api-display-objects/keto.yml @@ -0,0 +1,7 @@ +dsn: memory +log: + level: trace + +namespaces: + - name: chats + id: 1 diff --git a/contrib/docs-code-samples/package-lock.json b/contrib/docs-code-samples/package-lock.json new file mode 100644 index 0000000..0cc55de --- /dev/null +++ b/contrib/docs-code-samples/package-lock.json @@ -0,0 +1,651 @@ +{ + "name": "docs-code-samples", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@grpc/grpc-js": "1.14.4", + "@ory/keto-grpc-client": "0.13.0-alpha.0", + "google-protobuf": "3.21.2" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@ory/keto-grpc-client": { + "version": "0.13.0-alpha.0", + "resolved": "https://registry.npmjs.org/@ory/keto-grpc-client/-/keto-grpc-client-0.13.0-alpha.0.tgz", + "integrity": "sha512-WN5nnQ7E5z4YgaAN4HleYlcrFZR0Lq7ShwK+97qG+zGrOYvZLGDrdTJSosKNc73en7/MsjQqtLbfk4eaBTnfTw==", + "dependencies": { + "@grpc/grpc-js": "1.9.6", + "google-protobuf": "^3.21.2" + } + }, + "node_modules/@ory/keto-grpc-client/node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz", + "integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==", + "requires": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "dependencies": { + "@grpc/proto-loader": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz", + "integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.5", + "yargs": "^17.7.2" + } + } + } + }, + "@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "requires": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + } + }, + "@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==" + }, + "@ory/keto-grpc-client": { + "version": "0.13.0-alpha.0", + "resolved": "https://registry.npmjs.org/@ory/keto-grpc-client/-/keto-grpc-client-0.13.0-alpha.0.tgz", + "integrity": "sha512-WN5nnQ7E5z4YgaAN4HleYlcrFZR0Lq7ShwK+97qG+zGrOYvZLGDrdTJSosKNc73en7/MsjQqtLbfk4eaBTnfTw==", + "requires": { + "@grpc/grpc-js": "1.9.16", + "google-protobuf": "^3.21.2" + }, + "dependencies": { + "@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "requires": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + } + } + } + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==" + }, + "@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==" + }, + "@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "requires": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==" + }, + "@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "requires": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "google-protobuf": { + "version": "3.21.2", + "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.21.2.tgz", + "integrity": "sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "requires": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==" + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + } + } +} diff --git a/contrib/docs-code-samples/package.json b/contrib/docs-code-samples/package.json new file mode 100644 index 0000000..2663681 --- /dev/null +++ b/contrib/docs-code-samples/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "type": "module", + "scripts": { + "test": "./test.sh" + }, + "dependencies": { + "@grpc/grpc-js": "1.14.4", + "@ory/keto-grpc-client": "0.13.0-alpha.0", + "google-protobuf": "3.21.2" + }, + "overrides": { + "@ory/keto-grpc-client": { + "@grpc/grpc-js": "1.9.16" + } + } +} diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/cli.sh b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/cli.sh new file mode 100755 index 0000000..370cf4c --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/cli.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +echo "messages:02y_15_4w350m3#decypher@john" | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully created tuple" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh new file mode 100755 index 0000000..154daad --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/curl.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +relationtuple=' +{ + "namespace": "messages", + "object": "02y_15_4w350m3", + "relation": "decypher", + "subject_id": "john" +}' + +curl --fail --silent -X PUT \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data "$relationtuple" \ + http://127.0.0.1:4467/admin/relation-tuples > /dev/null \ + && echo "Successfully created tuple" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/expected_output.txt b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/expected_output.txt new file mode 100644 index 0000000..f35f94a --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuple diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/index.js b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/index.js new file mode 100644 index 0000000..d8f7bc6 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/index.js @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +const relationTuple = new relationTuples.RelationTuple() +relationTuple.setNamespace("messages") +relationTuple.setObject("02y_15_4w350m3") +relationTuple.setRelation("decypher") + +const sub = new relationTuples.Subject() +sub.setId("john") +relationTuple.setSubject(sub) + +const tupleDelta = new write.RelationTupleDelta() +tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) +tupleDelta.setRelationTuple(relationTuple) + +const writeRequest = new write.TransactRelationTuplesRequest() +writeRequest.addRelationTupleDeltas(tupleDelta) + +writeClient.transactRelationTuples(writeRequest, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuple") + } +}) diff --git a/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/main.go b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/main.go new file mode 100644 index 0000000..37557b4 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/00-write-direct-access/main.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + client := rts.NewWriteServiceClient(conn) + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: []*rts.RelationTupleDelta{ + { + Action: rts.RelationTupleDelta_ACTION_INSERT, + RelationTuple: &rts.RelationTuple{ + Namespace: "messages", + Object: "02y_15_4w350m3", + Relation: "decypher", + Subject: rts.NewSubjectID("john"), + }, + }, + }, + }) + if err != nil { + panic("Encountered error: " + err.Error()) + } + + fmt.Println("Successfully created tuple") +} diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/cli.sh b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/cli.sh new file mode 100755 index 0000000..183d7ee --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/cli.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto check john decypher messages:02y_15_4w350m3 --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh new file mode 100755 index 0000000..13917c2 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/curl.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "subject_id=john" \ + --data-urlencode "relation=decypher" \ + --data-urlencode "namespace=messages" \ + --data-urlencode "object=02y_15_4w350m3" \ + http://127.0.0.1:4466/relation-tuples/check \ + | jq -r 'if .allowed == true then "Allowed" else "Denied" end' diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/expected_output.txt b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/expected_output.txt new file mode 100644 index 0000000..915c2a1 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/expected_output.txt @@ -0,0 +1 @@ +Allowed diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/index.js b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/index.js new file mode 100644 index 0000000..1384b8d --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/index.js @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, check, checkService } from "@ory/keto-grpc-client" + +const checkClient = new checkService.CheckServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const checkRequest = new check.CheckRequest() +checkRequest.setNamespace("messages") +checkRequest.setObject("02y_15_4w350m3") +checkRequest.setRelation("decypher") + +const sub = new relationTuples.Subject() +sub.setId("john") +checkRequest.setSubject(sub) + +checkClient.check(checkRequest, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log(resp.getAllowed() ? "Allowed" : "Denied") + } +}) diff --git a/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/main.go b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/main.go new file mode 100644 index 0000000..b593998 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/01-check-direct-access/main.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err.Error()) + } + + client := rts.NewCheckServiceClient(conn) + + res, err := client.Check(context.Background(), &rts.CheckRequest{ + Namespace: "messages", + Object: "02y_15_4w350m3", + Relation: "decypher", + Subject: rts.NewSubjectID("john"), + }) + if err != nil { + panic(err.Error()) + } + + if res.Allowed { + fmt.Println("Allowed") + return + } + fmt.Println("Denied") +} diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh new file mode 100755 index 0000000..335ec9f --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/cli.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +relationtuple=' +{ + "namespace": "messages", + "object": "02y_15_4w350m3", + "relation": "decypher", + "subject_id": "john" +}' + +keto relation-tuple delete -f <(echo "$relationtuple") -q > /dev/null --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh new file mode 100755 index 0000000..9a3d143 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/curl.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +curl -X DELETE -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "subject_id=john" \ + --data-urlencode "relation=decypher" \ + --data-urlencode "namespace=messages" \ + --data-urlencode "object=02y_15_4w350m3" \ + -H 'Content-Type: application/json' \ + http://127.0.0.1:4467/admin/relation-tuples diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/expected_output.txt b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/expected_output.txt new file mode 100644 index 0000000..e69de29 diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/index.js b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/index.js new file mode 100644 index 0000000..1f7f249 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/index.js @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +const relationTuple = new relationTuples.RelationTuple() +relationTuple.setNamespace("messages") +relationTuple.setObject("02y_15_4w350m3") +relationTuple.setRelation("decypher") + +const sub = new relationTuples.Subject() +sub.setId("john") +relationTuple.setSubject(sub) + +const tupleDelta = new write.RelationTupleDelta() +tupleDelta.setAction(write.RelationTupleDelta.Action.ACTION_DELETE) +tupleDelta.setRelationTuple(relationTuple) + +const writeRequest = new write.TransactRelationTuplesRequest() +writeRequest.addRelationTupleDeltas(tupleDelta) + +writeClient.transactRelationTuples(writeRequest, (err) => { + if (err) { + console.log("Unexpected err", err) + return 1 + } +}) diff --git a/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/main.go b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/main.go new file mode 100644 index 0000000..65ad1f6 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/99-cleanup/main.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + defer func() { _ = wc.Close() }() + + wClient := rts.NewWriteServiceClient(wc) + _, err = wClient.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: []*rts.RelationTupleDelta{ + { + RelationTuple: &rts.RelationTuple{ + Namespace: "messages", + Object: "02y_15_4w350m3", + Relation: "decypher", + Subject: rts.NewSubjectID("john"), + }, + Action: rts.RelationTupleDelta_ACTION_DELETE, + }, + }, + }) + if err != nil { + panic(err) + } +} diff --git a/contrib/docs-code-samples/simple-access-check-guide/keto.yml b/contrib/docs-code-samples/simple-access-check-guide/keto.yml new file mode 100644 index 0000000..1525667 --- /dev/null +++ b/contrib/docs-code-samples/simple-access-check-guide/keto.yml @@ -0,0 +1,7 @@ +dsn: memory +log: + level: trace + +namespaces: + - name: messages + id: 1 diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/cli.sh new file mode 100755 index 0000000..88d593f --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/cli.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +printf '%s\n' \ + 'File:data.txt#viewer@user_alice' \ + 'File:data.txt#viewer@user_bob' \ + 'File:data.txt#viewer@apikey_ci-bot' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/curl.sh new file mode 100755 index 0000000..82e4094 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/curl.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +curl -X PATCH --fail --silent \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data '[ + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_alice"}}, + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_bob"}}, + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"apikey_ci-bot"}} + ]' \ + http://127.0.0.1:4467/admin/relation-tuples > /dev/null \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/expected_output.txt new file mode 100644 index 0000000..6306203 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuples diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/index.js new file mode 100644 index 0000000..a8cd496 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/index.js @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +function insertSubjectID(subjectID) { + const rt = new relationTuples.RelationTuple() + rt.setNamespace("File") + rt.setObject("data.txt") + rt.setRelation("viewer") + const sub = new relationTuples.Subject() + sub.setId(subjectID) + rt.setSubject(sub) + const delta = new write.RelationTupleDelta() + delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + delta.setRelationTuple(rt) + return delta +} + +const req = new write.TransactRelationTuplesRequest() +for (const id of ["user_alice", "user_bob", "apikey_ci-bot"]) { + req.addRelationTupleDeltas(insertSubjectID(id)) +} + +writeClient.transactRelationTuples(req, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuples") + } +}) diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/main.go new file mode 100644 index 0000000..660269a --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/00-write-subject-id/main.go @@ -0,0 +1,48 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewWriteServiceClient(conn) + + insert := func(subjectID string) *rts.RelationTupleDelta { + return &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_INSERT, + RelationTuple: &rts.RelationTuple{ + Namespace: "File", + Object: "data.txt", + Relation: "viewer", + Subject: rts.NewSubjectID(subjectID), + }, + } + } + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: []*rts.RelationTupleDelta{ + insert("user_alice"), + insert("user_bob"), + insert("apikey_ci-bot"), + }, + }) + if err != nil { + panic(err) + } + + fmt.Println("Successfully created tuples") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/cli.sh new file mode 100755 index 0000000..a3ef95c --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/cli.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto check user_alice viewer File:data.txt --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/curl.sh new file mode 100755 index 0000000..8edc5ae --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/curl.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=File" \ + --data-urlencode "object=data.txt" \ + --data-urlencode "relation=viewer" \ + --data-urlencode "subject_id=user_alice" \ + http://127.0.0.1:4466/relation-tuples/check \ + | jq -r 'if .allowed == true then "Allowed" else "Denied" end' diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/expected_output.txt new file mode 100644 index 0000000..915c2a1 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/expected_output.txt @@ -0,0 +1 @@ +Allowed diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/index.js new file mode 100644 index 0000000..afcda43 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/index.js @@ -0,0 +1,26 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, check, checkService } from "@ory/keto-grpc-client" + +const checkClient = new checkService.CheckServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const req = new check.CheckRequest() +req.setNamespace("File") +req.setObject("data.txt") +req.setRelation("viewer") +const sub = new relationTuples.Subject() +sub.setId("user_alice") +req.setSubject(sub) + +checkClient.check(req, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log(resp.getAllowed() ? "Allowed" : "Denied") + } +}) diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/main.go new file mode 100644 index 0000000..a7e48f6 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/01-check-subject-id/main.go @@ -0,0 +1,39 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewCheckServiceClient(conn) + + res, err := client.Check(context.Background(), &rts.CheckRequest{ + Namespace: "File", + Object: "data.txt", + Relation: "viewer", + Subject: rts.NewSubjectID("user_alice"), + }) + if err != nil { + panic(err) + } + + if res.Allowed { + fmt.Println("Allowed") + return + } + fmt.Println("Denied") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/cli.sh new file mode 100755 index 0000000..ca00247 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/cli.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +# During the migration phase, write every new tuple as both SubjectID and SubjectSet. +printf '%s\n' \ + 'File:data.txt#viewer@user_charlie' \ + 'File:data.txt#viewer@User:charlie' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/curl.sh new file mode 100755 index 0000000..c1d5b79 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/curl.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +# During migration, write every new tuple as both SubjectID and SubjectSet. +curl -X PATCH --fail --silent \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data '[ + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_charlie"}}, + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"charlie","relation":""}}} + ]' \ + http://127.0.0.1:4467/admin/relation-tuples > /dev/null \ + && echo "Successfully created tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/expected_output.txt new file mode 100644 index 0000000..6306203 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/expected_output.txt @@ -0,0 +1 @@ +Successfully created tuples diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/index.js new file mode 100644 index 0000000..1ee98b8 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/index.js @@ -0,0 +1,45 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +// During migration, write every new tuple as both SubjectID and SubjectSet. +function makeDelta(subject) { + const rt = new relationTuples.RelationTuple() + rt.setNamespace("File") + rt.setObject("data.txt") + rt.setRelation("viewer") + rt.setSubject(subject) + const delta = new write.RelationTupleDelta() + delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + delta.setRelationTuple(rt) + return delta +} + +const subjectID = new relationTuples.Subject() +subjectID.setId("user_charlie") + +const ss = new relationTuples.SubjectSet() +ss.setNamespace("User") +ss.setObject("charlie") +ss.setRelation("") +const subjectSet = new relationTuples.Subject() +subjectSet.setSet(ss) + +const req = new write.TransactRelationTuplesRequest() +req.addRelationTupleDeltas(makeDelta(subjectID)) +req.addRelationTupleDeltas(makeDelta(subjectSet)) + +writeClient.transactRelationTuples(req, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully created tuples") + } +}) diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/main.go new file mode 100644 index 0000000..cc4476b --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/02-write-both/main.go @@ -0,0 +1,50 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewWriteServiceClient(conn) + + // During migration, write every new tuple as both SubjectID and SubjectSet. + // This keeps existing SubjectID checks working while the backfill runs. + // Here we simulate an incoming request granting Charlie viewer access. + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: []*rts.RelationTupleDelta{ + { + Action: rts.RelationTupleDelta_ACTION_INSERT, + RelationTuple: &rts.RelationTuple{ + Namespace: "File", Object: "data.txt", Relation: "viewer", + Subject: rts.NewSubjectID("user_charlie"), + }, + }, + { + Action: rts.RelationTupleDelta_ACTION_INSERT, + RelationTuple: &rts.RelationTuple{ + Namespace: "File", Object: "data.txt", Relation: "viewer", + Subject: rts.NewSubjectSet("User", "charlie", ""), + }, + }, + }, + }) + if err != nil { + panic(err) + } + + fmt.Println("Successfully created tuples") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/cli.sh new file mode 100755 index 0000000..25f54cd --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/cli.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +# In production, paginate and filter all SubjectID tuples before writing back. +# This script writes the known SubjectSet tuples directly for brevity. +printf '%s\n' \ + 'File:data.txt#viewer@User:alice#' \ + 'File:data.txt#viewer@User:bob#' \ + 'File:data.txt#viewer@ApiKey:ci-bot#' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple create -f - >/dev/null --insecure-disable-transport-security \ + && echo "Migration complete" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/curl.sh new file mode 100755 index 0000000..45eed86 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/curl.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -euo pipefail + +# In production, paginate GET /admin/relation-tuples?namespace=File&relation=viewer, +# filter for tuples with subject_id set, and write SubjectSet counterparts in batches. +# This script writes the known SubjectSet tuples directly for brevity. + +curl -X PATCH --fail --silent \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data '[ + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"alice","relation":""}}}, + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"User","object":"bob","relation":""}}}, + {"action":"insert","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_set":{"namespace":"ApiKey","object":"ci-bot","relation":""}}} + ]' \ + http://127.0.0.1:4467/admin/relation-tuples > /dev/null \ + && echo "Migration complete" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/expected_output.txt new file mode 100644 index 0000000..2436058 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/expected_output.txt @@ -0,0 +1 @@ +Migration complete diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/index.js new file mode 100644 index 0000000..4f340d4 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/index.js @@ -0,0 +1,103 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { + relationTuples, + read, + readService, + write, + writeService, +} from "@ory/keto-grpc-client" + +const BATCH_SIZE = 100 + +const readClient = new readService.ReadServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +function subjectIDToNamespacedSubject(id) { + if (id.startsWith("user_")) + return { namespace: "User", object: id.slice("user_".length) } + if (id.startsWith("apikey_")) + return { namespace: "ApiKey", object: id.slice("apikey_".length) } + throw new Error("unknown subject ID prefix: " + id) +} + +function listRelationTuples(req) { + return new Promise((resolve, reject) => { + readClient.listRelationTuples(req, (error, resp) => { + if (error) reject(error) + else resolve(resp) + }) + }) +} + +function transactRelationTuples(req) { + return new Promise((resolve, reject) => { + writeClient.transactRelationTuples(req, (error, resp) => { + if (error) reject(error) + else resolve(resp) + }) + }) +} + +// Step 1: Paginate all File#viewer tuples and collect those with a SubjectID. +// In production, persist these to a file or database before writing back, +// so the migration can be resumed if interrupted. +const toMigrate = [] +let pageToken = "" +do { + const req = new read.ListRelationTuplesRequest() + const query = new read.ListRelationTuplesRequest.Query() + query.setNamespace("File") + query.setRelation("viewer") + req.setQuery(query) + req.setPageSize(BATCH_SIZE) + if (pageToken) req.setPageToken(pageToken) + + const resp = await listRelationTuples(req) + for (const t of resp.getRelationTuplesList()) { + if (t.getSubject().getId()) { + toMigrate.push(t) + } + } + pageToken = resp.getNextPageToken() +} while (pageToken) + +// Step 2: Write SubjectSet counterparts in batches. +for (let i = 0; i < toMigrate.length; i += BATCH_SIZE) { + const batch = toMigrate.slice(i, i + BATCH_SIZE) + const req = new write.TransactRelationTuplesRequest() + + for (const t of batch) { + const { namespace, object } = subjectIDToNamespacedSubject( + t.getSubject().getId(), + ) + + const ss = new relationTuples.SubjectSet() + ss.setNamespace(namespace).setObject(object).setRelation("") + const sub = new relationTuples.Subject() + sub.setSet(ss) + + const rt = new relationTuples.RelationTuple() + rt.setNamespace(t.getNamespace()) + .setObject(t.getObject()) + .setRelation(t.getRelation()) + .setSubject(sub) + + const delta = new write.RelationTupleDelta() + delta.setAction(write.RelationTupleDelta.Action.ACTION_INSERT) + delta.setRelationTuple(rt) + req.addRelationTupleDeltas(delta) + } + + await transactRelationTuples(req) +} + +console.log("Migration complete") diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/main.go new file mode 100644 index 0000000..587bd97 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/03-migrate-existing/main.go @@ -0,0 +1,100 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "strings" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +const batchSize = 100 + +// subjectIDToNamespacedSubject converts a prefixed subject ID to a namespace and object. +// The prefix encodes the type: "user_" → User namespace, "apikey_" → ApiKey namespace. +func subjectIDToNamespacedSubject(id string) (namespace, object string) { + switch { + case strings.HasPrefix(id, "user_"): + return "User", strings.TrimPrefix(id, "user_") + case strings.HasPrefix(id, "apikey_"): + return "ApiKey", strings.TrimPrefix(id, "apikey_") + default: + panic("unknown subject ID prefix: " + id) + } +} + +func main() { + rc, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + wc, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + readClient := rts.NewReadServiceClient(rc) + writeClient := rts.NewWriteServiceClient(wc) + ctx := context.Background() + + // Step 1: Paginate all File#viewer tuples and collect those with a SubjectID. + // In production, persist these to a file, SQLite, or a database before writing back, + // so the migration can be resumed if interrupted. + var toMigrate []*rts.RelationTuple + var pageToken string + for { + resp, err := readClient.ListRelationTuples(ctx, &rts.ListRelationTuplesRequest{ + RelationQuery: &rts.RelationQuery{ + Namespace: new("File"), + Relation: new("viewer"), + }, + PageToken: pageToken, + PageSize: batchSize, + }) + if err != nil { + panic(err) + } + for _, t := range resp.RelationTuples { + if t.Subject.GetId() != "" { + toMigrate = append(toMigrate, t) + } + } + if resp.NextPageToken == "" { + break + } + pageToken = resp.NextPageToken + } + // Step 2: Write SubjectSet counterparts in batches. + for i := 0; i < len(toMigrate); i += batchSize { + end := min(i+batchSize, len(toMigrate)) + batch := toMigrate[i:end] + + deltas := make([]*rts.RelationTupleDelta, len(batch)) + for j, t := range batch { + ns, obj := subjectIDToNamespacedSubject(t.Subject.GetId()) + deltas[j] = &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_INSERT, + RelationTuple: &rts.RelationTuple{ + Namespace: t.Namespace, + Object: t.Object, + Relation: t.Relation, + Subject: rts.NewSubjectSet(ns, obj, ""), + }, + } + } + + if _, err := writeClient.TransactRelationTuples(ctx, &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }); err != nil { + panic(err) + } + } + + fmt.Println("Migration complete") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/cli.sh new file mode 100755 index 0000000..bf86855 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/cli.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -euo pipefail + +export KETO_READ_REMOTE="127.0.0.1:4466" + +keto check User:alice viewer File:data.txt --insecure-disable-transport-security diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/curl.sh new file mode 100755 index 0000000..5df6da3 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/curl.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +curl -G --silent \ + --retry 7 --retry-connrefused \ + --data-urlencode "namespace=File" \ + --data-urlencode "object=data.txt" \ + --data-urlencode "relation=viewer" \ + --data-urlencode "subject_set.namespace=User" \ + --data-urlencode "subject_set.object=alice" \ + --data-urlencode "subject_set.relation=" \ + http://127.0.0.1:4466/relation-tuples/check \ + | jq -r 'if .allowed == true then "Allowed" else "Denied" end' diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/expected_output.txt new file mode 100644 index 0000000..915c2a1 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/expected_output.txt @@ -0,0 +1 @@ +Allowed diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/index.js new file mode 100644 index 0000000..978be0b --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/index.js @@ -0,0 +1,31 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, check, checkService } from "@ory/keto-grpc-client" + +const checkClient = new checkService.CheckServiceClient( + "127.0.0.1:4466", + grpc.credentials.createInsecure(), +) + +const ss = new relationTuples.SubjectSet() +ss.setNamespace("User") +ss.setObject("alice") +ss.setRelation("") +const sub = new relationTuples.Subject() +sub.setSet(ss) + +const req = new check.CheckRequest() +req.setNamespace("File") +req.setObject("data.txt") +req.setRelation("viewer") +req.setSubject(sub) + +checkClient.check(req, (error, resp) => { + if (error) { + console.log("Encountered error:", error) + } else { + console.log(resp.getAllowed() ? "Allowed" : "Denied") + } +}) diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/main.go new file mode 100644 index 0000000..4e59c6f --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/04-check-subject-set/main.go @@ -0,0 +1,39 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4466", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewCheckServiceClient(conn) + + res, err := client.Check(context.Background(), &rts.CheckRequest{ + Namespace: "File", + Object: "data.txt", + Relation: "viewer", + Subject: rts.NewSubjectSet("User", "alice", ""), + }) + if err != nil { + panic(err) + } + + if res.Allowed { + fmt.Println("Allowed") + return + } + fmt.Println("Denied") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/cli.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/cli.sh new file mode 100755 index 0000000..1e08363 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/cli.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -euo pipefail + +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +printf '%s\n' \ + 'File:data.txt#viewer@user_alice' \ + 'File:data.txt#viewer@user_bob' \ + 'File:data.txt#viewer@user_charlie' \ + 'File:data.txt#viewer@apikey_ci-bot' | \ + keto relation-tuple parse -f - --format json | \ + keto relation-tuple delete -f - >/dev/null --insecure-disable-transport-security \ + && echo "Successfully deleted SubjectID tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/curl.sh b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/curl.sh new file mode 100755 index 0000000..8539221 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/curl.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +curl -X PATCH --fail --silent \ + -H 'Content-Type: application/json' \ + --retry 7 --retry-connrefused \ + --data '[ + {"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_alice"}}, + {"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_bob"}}, + {"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"user_charlie"}}, + {"action":"delete","relation_tuple":{"namespace":"File","object":"data.txt","relation":"viewer","subject_id":"apikey_ci-bot"}} + ]' \ + http://127.0.0.1:4467/admin/relation-tuples > /dev/null \ + && echo "Successfully deleted SubjectID tuples" \ + || echo "Encountered error" diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/expected_output.txt b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/expected_output.txt new file mode 100644 index 0000000..853bd4d --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/expected_output.txt @@ -0,0 +1 @@ +Successfully deleted SubjectID tuples diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/index.js b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/index.js new file mode 100644 index 0000000..076a123 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/index.js @@ -0,0 +1,65 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import grpc from "@ory/keto-grpc-client/node_modules/@grpc/grpc-js/build/src/index.js" +import { relationTuples, write, writeService } from "@ory/keto-grpc-client" + +const writeClient = new writeService.WriteServiceClient( + "127.0.0.1:4467", + grpc.credentials.createInsecure(), +) + +// subjectIdTuples are the SubjectID tuples collected and persisted during the migration step. +// In production, load this list from the file or database written during migration so that +// you can target deletion of the tuples that were already migrated, and resume if interrupted. +const subjectIdTuples = [ + { + namespace: "File", + object: "data.txt", + relation: "viewer", + subjectId: "user_alice", + }, + { + namespace: "File", + object: "data.txt", + relation: "viewer", + subjectId: "user_bob", + }, + { + namespace: "File", + object: "data.txt", + relation: "viewer", + subjectId: "user_charlie", + }, + { + namespace: "File", + object: "data.txt", + relation: "viewer", + subjectId: "apikey_ci-bot", + }, +] + +const req = new write.TransactRelationTuplesRequest() +for (const { namespace, object, relation, subjectId } of subjectIdTuples) { + const sub = new relationTuples.Subject() + sub.setId(subjectId) + + const rt = new relationTuples.RelationTuple() + rt.setNamespace(namespace) + .setObject(object) + .setRelation(relation) + .setSubject(sub) + + const delta = new write.RelationTupleDelta() + delta.setAction(write.RelationTupleDelta.Action.ACTION_DELETE) + delta.setRelationTuple(rt) + req.addRelationTupleDeltas(delta) +} + +writeClient.transactRelationTuples(req, (error) => { + if (error) { + console.log("Encountered error", error) + } else { + console.log("Successfully deleted SubjectID tuples") + } +}) diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/main.go b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/main.go new file mode 100644 index 0000000..5724467 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/05-delete-subject-id/main.go @@ -0,0 +1,51 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +// subjectIdTuples are the SubjectID tuples collected and persisted during migration step. +// In production, load this list from the file or database written during migration so that +// you can target deletion of the tuples that were already migrated, and resume if interrupted. +var subjectIdTuples = []*rts.RelationTuple{ + {Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_alice")}, + {Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_bob")}, + {Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("user_charlie")}, + {Namespace: "File", Object: "data.txt", Relation: "viewer", Subject: rts.NewSubjectID("apikey_ci-bot")}, +} + +func main() { + conn, err := grpc.NewClient("127.0.0.1:4467", grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + panic(err) + } + + client := rts.NewWriteServiceClient(conn) + + deltas := make([]*rts.RelationTupleDelta, len(subjectIdTuples)) + + for i, t := range subjectIdTuples { + deltas[i] = &rts.RelationTupleDelta{ + Action: rts.RelationTupleDelta_ACTION_DELETE, + RelationTuple: t, + } + } + + _, err = client.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + if err != nil { + panic(err) + } + + fmt.Println("Successfully deleted SubjectID tuples") +} diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/keto.yml b/contrib/docs-code-samples/subject-id-to-subjectset-migration/keto.yml new file mode 100644 index 0000000..bbe1b66 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/keto.yml @@ -0,0 +1,6 @@ +dsn: memory +log: + level: trace + +namespaces: + location: file://./subject-id-to-subjectset-migration/namespaces.ts diff --git a/contrib/docs-code-samples/subject-id-to-subjectset-migration/namespaces.ts b/contrib/docs-code-samples/subject-id-to-subjectset-migration/namespaces.ts new file mode 100644 index 0000000..1aae977 --- /dev/null +++ b/contrib/docs-code-samples/subject-id-to-subjectset-migration/namespaces.ts @@ -0,0 +1,17 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Context, Namespace } from "@ory/keto-namespace-types" + +class User implements Namespace {} +class ApiKey implements Namespace {} + +class File implements Namespace { + related: { + viewer: (User | ApiKey)[] + } + + permits = { + view: (ctx: Context): boolean => this.related.viewer.includes(ctx.subject), + } +} diff --git a/contrib/docs-code-samples/test.sh b/contrib/docs-code-samples/test.sh new file mode 100755 index 0000000..518844d --- /dev/null +++ b/contrib/docs-code-samples/test.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail + +go -C ../.. install . +PATH="../../.bin/gobin:../../.bin/brew/bin:../../.bin/brew/sbin:$PATH" + +function teardown() { + kill "$keto_server_pid" || true +} +trap teardown EXIT + +function compare() { + local actual="$1" + local expected="$2/expected_output.json" + local d="jd -set" + if [[ ! -f "$expected" ]]; then + expected="$2/expected_output.txt" + d="diff -U 100000" + fi + $d "$actual" "$expected" +} + +for suite in */ ; do + suite=$(basename "$suite") + if [ "$suite" == "node_modules" ]; then + continue + fi + + keto serve -c "$suite/keto.yml" &> "serve_$suite.log" & + keto_server_pid=$! + + echo "starting keto server for suite $suite" + curl --retry 7 --retry-connrefused --output /dev/null --silent --fail http://127.0.0.1:4466/health/ready + + for main in "$suite"/*/main.go; do + echo "Running $main" + compare <(go run -tags docscodesamples "./$main" 2>&1) "$(dirname "$main")" + done + + for index in "$suite"/*/index.js; do + echo "Running $index" + compare <(node "$index" 2>&1) "$(dirname "$index")" + done + + for tc in "$suite"/*/curl.sh; do + echo "Running $tc" + compare <("$tc" 2>&1) "$(dirname "$tc")" + done + + for tc in "$suite"/*/cli.sh; do + echo "Running $tc" + compare <("$tc" 2>&1) "$(dirname "$tc")" + done + + kill "$keto_server_pid" +done + +echo +echo "EVERYTHING PASSED" +echo diff --git a/contrib/namespace-type-lib/index.d.ts b/contrib/namespace-type-lib/index.d.ts new file mode 100644 index 0000000..ad5c717 --- /dev/null +++ b/contrib/namespace-type-lib/index.d.ts @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +/// + +declare interface Boolean {} +declare interface String {} +declare interface Number {} +declare interface Function {} +declare interface Object {} +declare interface IArguments {} +declare interface RegExp {} + +declare interface Array { + /** + * Checks whether the elements of this Array have a Relation to the given Subject + * @example + * class File implements Namespace { + * related: { + * owners: (User | SubjectSet)[] + * } + * + * permits = { + * edit: (ctx: Context) => this.related.owners.includes(ctx.subject), + * } + * } + * @param element usually `ctx.subject` + */ + includes(element: T): boolean + + /** + * Executes the {@link iteratorfn} on every element in the Array and evaluates to true if {@link iteratorfn} returns true for any element. + * + * @example + * class File implements Namespace { + * related: { + * parents: (File | Folder)[] + * } + * + * permits = { + * view: (ctx: Context): boolean => + * // Checks whether the given context (e.g. subject) has view permissions on any of the parents, + * // effectively inhertiting the view permissions of the parents + * this.related.parents.traverse((p) => p.permits.view(ctx)) + * } + * } + * + * @param iteratorfn The function that checks if a connection exits + */ + traverse(iteratorfn: (element: T) => boolean): boolean +} + +interface context { + subject: never +} + +interface namespace { + /** + * Possible Relations to Objects of Namespaces + */ + related?: { [relation: string]: namespace[] } + + /** + * Dynamically computed Relations + */ + permits?: { [method: string]: (ctx: context) => boolean } +} + +declare module "@ory/keto-namespace-types" { + export type Context = context + + export type Namespace = namespace + + export type SubjectSet = + A["related"][R] extends Array ? T : never +} diff --git a/contrib/namespace-type-lib/package-lock.json b/contrib/namespace-type-lib/package-lock.json new file mode 100644 index 0000000..6e1c473 --- /dev/null +++ b/contrib/namespace-type-lib/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "@ory/keto-namespace-types", + "version": "0.9.0-alpha.0", + "lockfileVersion": 2, + "requires": true, + "dev": true, + "packages": { + "": { + "name": "@ory/keto-namespace-types", + "version": "0.9.0-alpha.0", + "devDependencies": { + "@ory/keto-namespace-types": "file:./", + "typescript": "^4.7.4" + } + }, + "node_modules/@ory/keto-namespace-types": { + "resolved": "", + "link": true + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@ory/keto-namespace-types": { + "version": "file:", + "requires": { + "@ory/keto-namespace-types": "file:", + "typescript": "^4.7.4" + }, + "dependencies": { + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + } + } + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + } + } +} diff --git a/contrib/namespace-type-lib/package.json b/contrib/namespace-type-lib/package.json new file mode 100644 index 0000000..f663467 --- /dev/null +++ b/contrib/namespace-type-lib/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ory/keto-namespace-types", + "version": "0.9.0-alpha.0", + "description": "TypeScript definitions for Ory Keto Namespaces", + "homepage": "https://www.ory.com/keto", + "bugs": "https://github.com/ory/keto/issues", + "main": "", + "types": "index.d.ts", + "files": [ + "index.d.ts", + "tsconfig.json" + ], + "scripts": { + "test": "tsc --types --noEmit --noLib test.ts" + }, + "dependencies": {}, + "devDependencies": { + "@ory/keto-namespace-types": "file:./", + "typescript": "^4.7.4" + } +} diff --git a/contrib/namespace-type-lib/test.ts b/contrib/namespace-type-lib/test.ts new file mode 100644 index 0000000..10f50de --- /dev/null +++ b/contrib/namespace-type-lib/test.ts @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Namespace, SubjectSet, Context } from "@ory/keto-namespace-types" + +// This test is not really a valid config, but rather a check of the types. +class User implements Namespace { + related: { + friends: User[] + } +} + +class Group implements Namespace { + related: { + members: (User | Group)[] + } + + permits = { + isMember: (ctx: Context): boolean => + this.related.members.traverse((m) => + m instanceof User ? m == ctx.subject : m.permits.isMember(ctx), + ), + } +} + +class File implements Namespace { + related: { + viewers: (User | SubjectSet)[] + } + + permits = { + view: (ctx: Context): boolean => + this.related.viewers.traverse((p) => + p instanceof User + ? p.related.friends.includes(ctx.subject) + : p.permits.isMember(ctx), + ) || this.related.viewers.includes(ctx.subject), + } +} diff --git a/contrib/namespace-type-lib/tsconfig.json b/contrib/namespace-type-lib/tsconfig.json new file mode 100644 index 0000000..7599d6b --- /dev/null +++ b/contrib/namespace-type-lib/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "noLib": true, + "noEmit": true, + "types": [] + } +} diff --git a/contrib/rewrites-example/keto.yaml b/contrib/rewrites-example/keto.yaml new file mode 100644 index 0000000..8e1fba1 --- /dev/null +++ b/contrib/rewrites-example/keto.yaml @@ -0,0 +1,7 @@ +dsn: memory + +namespaces: + location: file://./namespaces.keto.ts + +log: + level: trace diff --git a/contrib/rewrites-example/namespaces.keto.ts b/contrib/rewrites-example/namespaces.keto.ts new file mode 100644 index 0000000..7ae9cae --- /dev/null +++ b/contrib/rewrites-example/namespaces.keto.ts @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import { Namespace, SubjectSet, Context } from "@ory/keto-namespace-types" + +class User implements Namespace { + related: { + manager: User[] + } +} + +class Group implements Namespace { + related: { + members: (User | Group)[] + } +} + +class Folder implements Namespace { + related: { + parents: (File | Folder)[] + viewers: SubjectSet[] + } + + permits = { + view: (ctx: Context): boolean => + this.related.viewers.includes(ctx.subject) || + this.related.parents.traverse((p) => p.permits.view(ctx)), + } +} + +class File implements Namespace { + related: { + parents: (File | Folder)[] + viewers: (User | SubjectSet)[] + owners: (User | SubjectSet)[] + } + + // Some comment + permits = { + view: (ctx: Context): boolean => + this.related.parents.traverse((p) => p.permits.view(ctx)) || + this.related.viewers.includes(ctx.subject) || + this.related.owners.includes(ctx.subject), + + edit: (ctx: Context) => this.related.owners.includes(ctx.subject), + } +} diff --git a/contrib/rewrites-example/package-lock.json b/contrib/rewrites-example/package-lock.json new file mode 100644 index 0000000..5fe4940 --- /dev/null +++ b/contrib/rewrites-example/package-lock.json @@ -0,0 +1,58 @@ +{ + "name": "rewrites-example", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "rewrites-example", + "devDependencies": { + "@ory/keto-namespace-types": "file:../namespace-type-lib", + "typescript": "latest" + } + }, + "../../../../../../namespace-type-lib": { + "extraneous": true + }, + "../namespace-type-lib": { + "name": "@ory/keto-namespace-types", + "version": "0.9.0-alpha.0", + "dev": true, + "devDependencies": { + "@ory/keto-namespace-types": "file:./", + "typescript": "^4.7.4" + } + }, + "node_modules/@ory/keto-namespace-types": { + "resolved": "../namespace-type-lib", + "link": true + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@ory/keto-namespace-types": { + "version": "file:../namespace-type-lib", + "requires": { + "@ory/keto-namespace-types": "file:", + "typescript": "^4.7.4" + } + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + } + } +} diff --git a/contrib/rewrites-example/package.json b/contrib/rewrites-example/package.json new file mode 100644 index 0000000..f2eed3d --- /dev/null +++ b/contrib/rewrites-example/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "devDependencies": { + "@ory/keto-namespace-types": "file:../namespace-type-lib", + "typescript": "latest" + } +} diff --git a/contrib/rewrites-example/relation-tuples/tuples.json b/contrib/rewrites-example/relation-tuples/tuples.json new file mode 100644 index 0000000..4982fb6 --- /dev/null +++ b/contrib/rewrites-example/relation-tuples/tuples.json @@ -0,0 +1,66 @@ +[ + { + "namespace": "Group", + "object": "developer", + "relation": "members", + "subject_set": { + "namespace": "User", + "object": "Patrik" + } + }, + { + "namespace": "Group", + "object": "developer", + "relation": "members", + "subject_set": { + "namespace": "User", + "object": "Henning" + } + }, + { + "namespace": "Folder", + "object": "keto/", + "relation": "viewers", + "subject_set": { + "namespace": "Group", + "object": "developer", + "relation": "members" + } + }, + { + "namespace": "File", + "object": "keto/README.md", + "relation": "parents", + "subject_set": { + "namespace": "Folder", + "object": "keto/" + } + }, + { + "namespace": "Folder", + "object": "keto/src/", + "relation": "parents", + "subject_set": { + "namespace": "Folder", + "object": "keto/" + } + }, + { + "namespace": "File", + "object": "keto/src/main.go", + "relation": "parents", + "subject_set": { + "namespace": "Folder", + "object": "keto/src/" + } + }, + { + "namespace": "File", + "object": "private", + "relation": "owners", + "subject_set": { + "namespace": "User", + "object": "Henning" + } + } +] diff --git a/contrib/rewrites-example/tsconfig.json b/contrib/rewrites-example/tsconfig.json new file mode 100644 index 0000000..444513b --- /dev/null +++ b/contrib/rewrites-example/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@ory/keto-namespace-types/tsconfig.json" +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..08eeb9b --- /dev/null +++ b/doc.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package main ORY Keto +// +// Ory Keto is a cloud native access control server providing best-practice patterns (RBAC, ABAC, ACL, AWS IAM Policies, Kubernetes Roles, ...) via REST APIs. +// +// Schemes: http, https +// Host: +// BasePath: / +// Version: Latest +// License: Apache 2.0 https://github.com/ory/keto/blob/master/LICENSE +// Contact: ORY https://www.ory.com +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// swagger:meta +package main diff --git a/doc_swagger.go b/doc_swagger.go new file mode 100644 index 0000000..8278513 --- /dev/null +++ b/doc_swagger.go @@ -0,0 +1,26 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import "github.com/ory/herodot" + +// JSON API Error Response +// +// The standard Ory JSON API error format. +// +// swagger:model errorGeneric +type _ struct { + // Contains error details + // + // required: true + Error genericError `json:"error"` +} + +// swagger:model genericError +type genericError struct{ herodot.DefaultError } + +// Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204. +// +// swagger:response emptyResponse +type _ struct{} diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml new file mode 100644 index 0000000..976606b --- /dev/null +++ b/docker-compose-build.yaml @@ -0,0 +1,21 @@ +version: "3" + +services: + keto: + build: + context: "." + dockerfile: ".docker/Dockerfile-build" + ports: + - "4466:4466" + - "4467:4467" + - "4468:4468" + command: serve -c /home/ory/keto.yml + restart: on-failure + volumes: + - type: bind + source: "./config" + target: "/home/ory" + environment: + - LOG_LEVEL=debug + - PORT=4466 + - DSN=memory diff --git a/docker-compose-mysql.yml b/docker-compose-mysql.yml new file mode 100644 index 0000000..64703d3 --- /dev/null +++ b/docker-compose-mysql.yml @@ -0,0 +1,35 @@ +version: "3.2" +services: + keto-migrate: + image: oryd/keto:v26.2.0 + links: + - mysqld:mysqld + volumes: + - type: bind + source: ./config + target: /home/ory + environment: + - LOG_LEVEL=debug + - DSN=mysql://root:secret@mysqld:3306/mysql&max_conns=20&max_idle_conns=4 + command: ["migrate", "up", "-y"] + restart: on-failure + keto: + links: + - mysqld:mysqld + volumes: + - type: bind + source: ./config + target: /home/ory + ports: + - "4466:4466" + - "4467:4467" + depends_on: + - keto-migrate + environment: + - DSN=mysql://root:secret@mysqld:3306/mysql&max_conns=20&max_idle_conns=4 + mysqld: + image: mysql:5.7 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD=secret diff --git a/docker-compose-postgres.yml b/docker-compose-postgres.yml new file mode 100644 index 0000000..9fbf184 --- /dev/null +++ b/docker-compose-postgres.yml @@ -0,0 +1,37 @@ +version: "3.2" +services: + keto-migrate: + image: oryd/keto:v26.2.0 + links: + - postgresd:postgresd + volumes: + - type: bind + source: ./config + target: /home/ory + environment: + - LOG_LEVEL=debug + - DSN=postgres://dbuser:secret@postgresd:5432/accesscontroldb?sslmode=disable + command: ["migrate", "up", "-y"] + restart: on-failure + keto: + image: oryd/keto:v0.11.1-alpha.0 + links: + - postgresd:postgresd + volumes: + - type: bind + source: ./config + target: /home/ory + ports: + - "4466:4466" + - "4467:4467" + depends_on: + - keto-migrate + environment: + - DSN=postgres://dbuser:secret@postgresd:5432/accesscontroldb?sslmode=disable + restart: on-failure + postgresd: + image: postgres:9.6 + environment: + - POSTGRES_USER=dbuser + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=accesscontroldb diff --git a/docker-compose-tracing.yml b/docker-compose-tracing.yml new file mode 100644 index 0000000..76ac156 --- /dev/null +++ b/docker-compose-tracing.yml @@ -0,0 +1,31 @@ +########################################################################### +####### FOR DEMONSTRATION PURPOSES ONLY ####### +########################################################################### +# # +# If you have not yet read the tutorial, do so now: # +# https://www.ory.com/docs/hydra/5min-tutorial # +# # +# This set up is only for demonstration purposes. The login # +# endpoint can only be used if you follow the steps in the tutorial. # +# # +########################################################################### +version: "3" +services: + keto: + depends_on: + - jaeger + environment: + - TRACING_PROVIDER=jaeger + - TRACING_PROVIDER_JAEGER_SAMPLING_SERVER_URL=http://jaeger:5778/sampling + - TRACING_PROVIDER_JAEGER_LOCAL_AGENT_ADDRESS=jaeger:6831 + + jaeger: + image: jaegertracing/all-in-one:1.7.0 + ports: + - "5775:5775/udp" + - "6831:6831/udp" + - "6832:6832/udp" + - "5778:5778" + - "16686:16686" + - "14268:14268" + - "9411:9411" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5bd8c1f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3" +services: + keto: + image: oryd/keto:v26.2.0 + ports: + - "4466:4466" + - "4467:4467" + command: serve -c /home/ory/keto.yml + restart: on-failure + volumes: + - type: bind + source: config/keto.yml + target: /home/ory/keto.yml diff --git a/docs/.generated/proto-api.mdx b/docs/.generated/proto-api.mdx new file mode 100644 index 0000000..50368be --- /dev/null +++ b/docs/.generated/proto-api.mdx @@ -0,0 +1,301 @@ +--- +title: Protocol Buffers API +--- + + + +The API is divided into the +[read APIs](../concepts/25_api-overview.mdx#read-apis) and +[write APIs](../concepts/25_api-overview.mdx#write-apis). Each service is +annotated with the API it belongs to. + +## ory/keto/acl/v1alpha1/acl.proto + +### RelationTuple + +RelationTuple defines a relation between an Object and a Subject. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------- | +| namespace | [string](#string) | | The namespace this relation tuple lives in. | +| object | [string](#string) | | The object related by this tuple. It is an object in the namespace of the tuple. | +| relation | [string](#string) | | The relation between an Object and a Subject. | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | The subject related by this tuple. A Subject either represents a concrete subject id or a `SubjectSet` that expands to more Subjects. | + +### Subject + +Subject is either a concrete subject id or a `SubjectSet` expanding to more +Subjects. + +| Field | Type | Label | Description | +| ----- | ----------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------- | +| id | [string](#string) | | A concrete id of the subject. | +| set | [SubjectSet](#ory.keto.acl.v1alpha1.SubjectSet) | | A subject set that expands to more Subjects. More information are available under [concepts](../concepts/15_subjects.mdx). | + +### SubjectSet + +SubjectSet refers to all subjects who have the same `relation` on an `object`. + +| Field | Type | Label | Description | +| --------- | ----------------- | ----- | ------------------------------------------------------------------------ | +| namespace | [string](#string) | | The namespace of the object and relation referenced in this subject set. | +| object | [string](#string) | | The object related by this subject set. | +| relation | [string](#string) | | The relation between the object and the subjects. | + +## ory/keto/acl/v1alpha1/check_service.proto + +### CheckService + +The service that performs authorization checks based on the stored Access +Control Lists. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | --------------------------------------------------- | ----------------------------------------------------- | -------------------------------- | +| Check | [CheckRequest](#ory.keto.acl.v1alpha1.CheckRequest) | [CheckResponse](#ory.keto.acl.v1alpha1.CheckResponse) | Performs an authorization check. | + +### CheckRequest + +The request for a CheckService.Check RPC. Checks whether a specific subject is +related to an object. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| namespace | [string](#string) | | The namespace to evaluate the check.
Note: If you use the expand-API and the check evaluates a RelationTuple specifying a SubjectSet as subject or due to a rewrite rule in a namespace config this check request may involve other namespaces automatically. | +| object | [string](#string) | | The related object in this check. | +| relation | [string](#string) | | The relation between the Object and the Subject. | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | The related subject in this check. | +| latest | [bool](#bool) | | This field is not implemented yet and has no effect. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect. | +| max_depth | [int32](#int32) | | The maximum depth to search for a relation.
If the value is less than 1 or greater than the global max-depth then the global max-depth will be used instead. | + +### CheckResponse + +The response for a CheckService.Check rpc. + +| Field | Type | Label | Description | +| --------- | ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allowed | [bool](#bool) | | Whether the specified subject (id) is related to the requested object.
It is false by default if no ACL matches. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect. | + +## ory/keto/acl/v1alpha1/expand_service.proto + +### ExpandService + +The service that performs subject set expansion based on the stored Access +Control Lists. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | ----------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------ | +| Expand | [ExpandRequest](#ory.keto.acl.v1alpha1.ExpandRequest) | [ExpandResponse](#ory.keto.acl.v1alpha1.ExpandResponse) | Expands the subject set into a tree of subjects. | + +### ExpandRequest + +The request for an ExpandService.Expand RPC. Expands the given subject set. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | The subject to expand. | +| max_depth | [int32](#int32) | | The maximum depth of tree to build.
If the value is less than 1 or greater than the global max-depth then the global max-depth will be used instead.
It is important to set this parameter to a meaningful value. Ponder how deep you really want to display this. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect. | + +### ExpandResponse + +The response for a ExpandService.Expand RPC. + +| Field | Type | Label | Description | +| ----- | ------------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| tree | [SubjectTree](#ory.keto.acl.v1alpha1.SubjectTree) | | The tree the requested subject set expands to. The requested subject set is the subject of the root.
This field can be nil in some circumstances. | + +### SubjectTree + +| Field | Type | Label | Description | +| --------- | ------------------------------------------------- | -------- | ------------------------------------------------------------------------------------ | +| node_type | [NodeType](#ory.keto.acl.v1alpha1.NodeType) | | The type of the node. | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | The subject this node represents. | +| children | [SubjectTree](#ory.keto.acl.v1alpha1.SubjectTree) | repeated | The children of this node.
This is never set if `node_type` == `NODE_TYPE_LEAF`. | + +### NodeType + +| Name | Number | Description | +| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------- | +| NODE_TYPE_UNSPECIFIED | 0 | | +| NODE_TYPE_UNION | 1 | This node expands to a union of all children. | +| NODE_TYPE_EXCLUSION | 2 | Not implemented yet. | +| NODE_TYPE_INTERSECTION | 3 | Not implemented yet. | +| NODE_TYPE_LEAF | 4 | This node is a leaf and contains no children. Its subject is a `SubjectID` unless `max_depth` was reached. | + +## ory/keto/acl/v1alpha1/read_service.proto + +### ReadService + +The service to query relation tuples. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ------------------ | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------- | +| ListRelationTuples | [ListRelationTuplesRequest](#ory.keto.acl.v1alpha1.ListRelationTuplesRequest) | [ListRelationTuplesResponse](#ory.keto.acl.v1alpha1.ListRelationTuplesResponse) | Lists ACL relation tuples. | + +### ListRelationTuplesRequest + +Request for ReadService.ListRelationTuples RPC. See +`ListRelationTuplesRequest_Query` for how to filter the query. + +| Field | Type | Label | Description | +| ----------- | ----------------------------------------------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| query | [ListRelationTuplesRequest.Query](#ory.keto.acl.v1alpha1.ListRelationTuplesRequest.Query) | | All query constraints are concatenated with a logical AND operator.
The RelationTuple list from ListRelationTuplesResponse is ordered from the newest RelationTuple to the oldest. | +| expand_mask | [google.protobuf.FieldMask](#google.protobuf.FieldMask) | | This field is not implemented yet and has no effect. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect. | +| page_size | [int32](#int32) | | Optional. The maximum number of RelationTuples to return in the response.
Default: 100 | +| page_token | [string](#string) | | Optional. An opaque pagination token returned from a previous call to `ListRelationTuples` that indicates where the page should start at.
An empty token denotes the first page. All successive pages require the token from the previous page. | + +### ListRelationTuplesRequest.Query + +The query for listing relation tuples. Clients can specify any optional field to +partially filter for specific relation tuples. + +Example use cases (namespace is always required): + +- object only: display a list of all permissions referring to a specific object +- relation only: get all groups that have members; get all directories that have + content +- object & relation: display all subjects that have a specific permission + relation +- subject & relation: display all groups a subject belongs to; display all + objects a subject has access to +- object & relation & subject: check whether the relation tuple already exists + +| Field | Type | Label | Description | +| --------- | ----------------------------------------- | ----- | ------------------------------------ | +| namespace | [string](#string) | | Required. The namespace to query. | +| object | [string](#string) | | Optional. The object to query for. | +| relation | [string](#string) | | Optional. The relation to query for. | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | Optional. The subject to query for. | + +### ListRelationTuplesResponse + +The response of a ReadService.ListRelationTuples RPC. + +| Field | Type | Label | Description | +| --------------- | ----------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------ | +| relation_tuples | [RelationTuple](#ory.keto.acl.v1alpha1.RelationTuple) | repeated | The relation tuples matching the list request. | +| next_page_token | [string](#string) | | The token required to get the next page. If this is the last page, the token will be the empty string. | + +## ory/keto/acl/v1alpha1/version.proto + +### VersionService + +The service returning the specific Ory Keto instance version. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis) and +[write-APIs](../concepts/25_api-overview.mdx#write-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------------------------------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | +| GetVersion | [GetVersionRequest](#ory.keto.acl.v1alpha1.GetVersionRequest) | [GetVersionResponse](#ory.keto.acl.v1alpha1.GetVersionResponse) | Returns the version of the Ory Keto instance. | + +### GetVersionRequest + +Request for the VersionService.GetVersion RPC. + +### GetVersionResponse + +Response of the VersionService.GetVersion RPC. + +| Field | Type | Label | Description | +| ------- | ----------------- | ----- | -------------------------------------------- | +| version | [string](#string) | | The version string of the Ory Keto instance. | + +## ory/keto/acl/v1alpha1/write_service.proto + +### WriteService + +The write service to create and delete Access Control Lists. + +This service is part of the +[write-APIs](../concepts/25_api-overview.mdx#write-apis). + +| Method Name | Request Type | Response Type | Description | +| ---------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| TransactRelationTuples | [TransactRelationTuplesRequest](#ory.keto.acl.v1alpha1.TransactRelationTuplesRequest) | [TransactRelationTuplesResponse](#ory.keto.acl.v1alpha1.TransactRelationTuplesResponse) | Writes one or more relation tuples in a single transaction. | +| DeleteRelationTuples | [DeleteRelationTuplesRequest](#ory.keto.acl.v1alpha1.DeleteRelationTuplesRequest) | [DeleteRelationTuplesResponse](#ory.keto.acl.v1alpha1.DeleteRelationTuplesResponse) | Deletes relation tuples based on relation query | + +### DeleteRelationTuplesRequest + +| Field | Type | Label | Description | +| ----- | --------------------------------------------------------------------------------------------- | ----- | ----------- | +| query | [DeleteRelationTuplesRequest.Query](#ory.keto.acl.v1alpha1.DeleteRelationTuplesRequest.Query) | | | + +### DeleteRelationTuplesRequest.Query + +The query for deleting relation tuples + +| Field | Type | Label | Description | +| --------- | ----------------------------------------- | ----- | ------------------------------------ | +| namespace | [string](#string) | | Optional. The namespace to query. | +| object | [string](#string) | | Optional. The object to query for. | +| relation | [string](#string) | | Optional. The relation to query for. | +| subject | [Subject](#ory.keto.acl.v1alpha1.Subject) | | Optional. The subject to query for. | + +### DeleteRelationTuplesResponse + +### RelationTupleDelta + +Write-delta for a TransactRelationTuplesRequest. + +| Field | Type | Label | Description | +| -------------- | ----------------------------------------------------------------------------- | ----- | -------------------------------------- | +| action | [RelationTupleDelta.Action](#ory.keto.acl.v1alpha1.RelationTupleDelta.Action) | | The action to do on the RelationTuple. | +| relation_tuple | [RelationTuple](#ory.keto.acl.v1alpha1.RelationTuple) | | The target RelationTuple. | + +### TransactRelationTuplesRequest + +The request of a WriteService.TransactRelationTuples RPC. + +| Field | Type | Label | Description | +| --------------------- | --------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| relation_tuple_deltas | [RelationTupleDelta](#ory.keto.acl.v1alpha1.RelationTupleDelta) | repeated | The write delta for the relation tuples operated in one single transaction. Either all actions succeed or no change takes effect on error. | + +### TransactRelationTuplesResponse + +The response of a WriteService.TransactRelationTuples rpc. + +| Field | Type | Label | Description | +| ---------- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| snaptokens | [string](#string) | repeated | This field is not implemented yet and has no effect. | + +### RelationTupleDelta.Action + +| Name | Number | Description | +| ------------------ | ------ | ----------------------------------------------------------------------------------------------------------- | +| ACTION_UNSPECIFIED | 0 | Unspecified. The `TransactRelationTuples` RPC ignores this RelationTupleDelta if an action was unspecified. | +| INSERT | 1 | Insertion of a new RelationTuple. It is ignored if already existing. | +| DELETE | 2 | Deletion of the RelationTuple. It is ignored if it does not exist. | + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------- | ------- | ---------- | -------------- | ------------------------------ | +|
double | | double | double | float | float64 | double | float | Float | +| float | | float | float | float | float32 | float | float | Float | +| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | +| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | +| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | +| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | +| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | +| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | +| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..55fb2b5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# Documentation + +Please find the documentation at +[www.ory.com/docs/keto](https://www.ory.com/docs/keto). + +To contribute to the documentation, please head over to: +[github.com/ory/docs/tree/master/docs/keto](https://github.com/ory/docs/tree/master/docs/keto) diff --git a/docs/ory_permission_language_spec.md b/docs/ory_permission_language_spec.md new file mode 100644 index 0000000..c878738 --- /dev/null +++ b/docs/ory_permission_language_spec.md @@ -0,0 +1,358 @@ +# The "Ory Permission Language" Specification + +Enforcing fine-grained permissions is a critical building block of mature +technology solutions that protect privacy and identity in the information age. +Several proprietary languages to represent permission already exist, namely the +[Authzed Schema Language](https://docs.authzed.com/reference/schema-lang) and +[Auth0 FGA](https://docs.fga.dev/). Most permissions are defined by normal +developers who typically are most familiar with Web technologies like JavaScript +or Typescript. We see the need for a developer-friendly configuration language +for permissions that has such a small learning curve that most developers can +understand and use it with close to no effort. We therefore chose to define our +permissions configuration language as a subset of the most common +general-purpose programming language: JavaScript/TypeScript. + +The Ory Permission Language is a syntactical subset of TypeScript. Along with +type definitions for the syntax elements of the language (such as `Namespace` or +`Context`), users can get context help from their IDE while writing the +configuration. + +## Notation + +The syntax is specified using the Extended Backus-Naur Form (EBNF): + +```ebnf +Production = production_name "=" [ Expression ] "." . +Expression = Alternative { "|" Alternative } . +Alternative = Term { Term } . +Term = production_name | token [ "…" token ] | Group | Option | Repetition . +Group = "(" Expression ")" . +Option = "[" Expression "]" . +Repetition = "{" Expression "}" . +``` + +Productions are expressions constructed from terms and the following operators, +in increasing precedence: + +```ebnf +| alternation +() grouping +[] option (0 or 1 times) +{} repetition (0 to n times) +``` + +Lower-case production names are used to identify lexical tokens. Non-terminals +are in CamelCase. Lexical tokens are enclosed in double quotes `""` or single +quotes `''`. + +The form `a … b` represents the set of characters from a through b as +alternatives. The horizontal ellipsis `…` is also used elsewhere in the spec to +informally denote various enumerations or code snippets that are not further +specified. + +## Configuraton text representation + +The configuration is encoded in UTF-8. + +## Lexical elements + +### Comments + +1. Line comments start with the character sequence `//` and stop at the end of + the line. +2. General comments start with the character sequence `/*` and stop with the + first subsequent character sequence `*/`. +3. Documentation comments start with the character sequence `/**` and stop with + the first subsequent character sequence `*/`. + +### Identifiers + +Identifiers name program entities such as variables and types. An identifier is +a sequence of one or more letters and digits. The first character in an +identifier must be a letter. + +```ebnf +identifier = letter { letter | digit } . +digit = "0" … "9" . +letter = "A" … "Z" | "a" … "z" | "_" . +``` + +### String literals + +String literals represent string constants as sequences of characters. + +```ebnf +string_lit = single_quoted | double_quoted . +single_quoted = "'" identifier "'" . +double_quoted = '"' identifier '"' . +``` + +### Keywords + +The configuration language has the following keywords: + +- `class` +- `implements` +- `related` +- `permits` +- `this` +- `ctx` +- `id` +- `imports` +- `exports` +- `as` + +### Builtin Types + +The following types are built in: + +- `Context` +- `Namespace` +- `Namespace[]` +- `boolean` +- `string` +- `SubjectSet` + +In TypeScript, they would be defined as follows: + +```typescript +type Context = { subject: never } + +interface Namespace { + related?: { [relation: string]: Namespace[] } + permits?: { [method: string]: (ctx: Context) => boolean } +} + +interface Array { + includes(element: Namespace): boolean + traverse(iteratorfn: (element: Namespace) => boolean): boolean +} + +type SubjectSet = + A["related"][R] extends Array ? T : never +``` + +### Operators + +The following character sequences represent boolean operators: + +| Operator | Signature | Semantic | +| -------- | -------------- | ------------------------------------ | +| `&&` | _x_ `&&` _y_ | true iff. both _x_ and _y_ are true | +| `\|\|` | _x_ `\|\|` _y_ | true iff. either _x_ or _y_ are true | +| `!` | `!` _x_ | true iff. _x_ is false | + +The following character sequences represent miscellaneous operators: + +| Operator | Example | Semantic | +| -------- | ---------------------------------------- | ----------------------------------------------------------------- | +| `(`, `)` | `x()` | call function `x` | +| `[]` | `T[]` | `T` is an array type | +| `<`, `>` | `SubjectSet` | Type reference to the "members" relation of the "Group" namespace | +| `{`, `}` | `class x {}` | Scope delimiters | +| `=>` | `list.transitive(el => f(el))` | Lambda definition token. | +| `.` | `this.x` | Property traversal token | +| `:` | `relation: type` | Relation/type separator token | +| `=` | `permits = {...}` | Assignment token | +| `,` | `{x: 1, y: 2}` | Property separator | +| `'` | `'string'` | Single quoted string literal | +| `"` | `"string"` | Double quoted string literal | +| `*` | `import * from @ory/permission-language` | Import glob | +| `\|` | `(User \| Group)[]` | Type union | + +## Statements + +### Type declaration + +The top level of the configuration consists of a list of `class` declarations +for each namespace. Each `class` consists of relation declarations and +permission declarations. + +```ebnf +Config = [ ClassDecl ] . +ClassDecl = "class" identifier "implements" "Namespace" "{" ClassSpec "}" . +ClassSpec = [ RelationDecls ] | [ PermissionDefns] . +``` + +The following example declares the type _User_. + +```ts +class User implements Namespace {} +``` + +### Relation declaration + +The `related` section of type declarations defines relations. Unlike regular +TypeScript, `RelationName` must be a unique identifier used as the relation +strings in the tuples. The `TypeName` must be the name of another `type` that is +defined above or below (in TypeScript: a class that implements `Namespace`). + +Type unions (`|`) can be used to denote that a relation can have subjects of +multiple types, e.g., `viewers: (User | SubjectSet)[]`, +meaning that the subject of the "viewer" relation can be either a "User", or a +subject set "Group#members". + +```ebnf +RelationDecls = "related" "=" "{" { RelationName ":" ArrayType } "}" . +RelationName = identifier . +ArrayType = RelationType | ( "(" RelationType { "|" RelationType } ")" ) "[]" . +RelationType = SubjectType | SubjectSetType . +SubjectType = TypeName . +SubjectSetType = "SubjectSet" "<" TypeName, string_lit ">" . +TypeName = identifier . +``` + +Note that all relations are defined as array types `T[]` because there are +naturally only many-to-many relations in Keto. + +The following declares a type _Document_ with three relations: _owners_ and +_viewers_, both of which have _users_ as subjects. Additionally, the relation +_parent_ has type _Document_. + +```ts +class User {} + +class Document { + related = { + parents: Document[] + owners: User[] + viewers: User[] + } +} +``` + +### Permission definition + +Permissions are defined as functions within class declarations that take a +parameter `ctx` of type `Context` and evaluate to a boolean `true` or `false`. + +The type annotations for `ctx` and the return value are optional. + +```ebnf +PermissionDefns = "permits" "=" "{" Permission [ "," Permission ] "}" . +Permission = PermissionSign "=>" PermissionBody . +PermissionSign = PermissionName ":" "(" "ctx" [ ":" "Context" ] ")" [ ":" "boolean" ] . +PermissionName = identifier . +``` + +The `ctx` object is a fixed parameter that contains the `subject` for which the +permission check should be conducted: + +```ts +ctx = { subject: "some_user_id" } +``` + +The context will contain more fields in the future, e.g., the IP range or +geolocation, the time of day, or the security level of the device making the +request. + +```ebnf +PermissionBody = ( "(" PermissionBody ")" ) | ( "!" PermissionBody ) | ( PermissionCheck | { Operator PermissionBody } ) . +Operator = "||" | "&&" . +PermissionCheck = TransitiveCheck | IncludesCheck . +``` + +The body of a permission check is either one of: + +- a `IncludesCheck`, a check that something is in a set, e.g., + `this.related.viewers.includes(ctx.subject)`: + + ```ebnf + IncludesCheck = Var "." "related" "." RelationName "." "includes" "(" "ctx" "." "subject" ")" . + Var = identifier . + ``` + +- a `TranstitiveCheck`, a call to a permission on a relation, e.g., + `this.related.parents.transitive(p => p.permits.view(ctx))`: + + ```ebnf + TransitiveCheck = "this" "." "related" "." RelationName "." "transitive" "(" Var "=>" ( PermissionCall | IncludesCheck ) ")" . + PermissionCall = Var "." "permits" "." PermissionName "(" "ctx" ")" . + ``` + +## Implementation notes + +`IncludeCheck` and `TransitiveCheck` translate to Zanzibar concepts as follows: + +| Keto Config | Zanzibar AST | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------ | +| `this.related.R.includes(ctx.subject)` | `computed_userset { relation: "R" } }` | +| `this.related.R.transitive(x = x.permits.P(ctx.subject))` | `tuple_to_userset { tupleset { relation: "R" } computed_userset { relation: "P" } }` | + +## Type checking + +The following type checks are performed once the config is fully parsed: + +- Given a `TypeName` as `X` (e.g., in `RelationDecls`), we check that there + exists a class declaration for `X`. +- Given a `SubjectSetType` as `SubjectSet`, we check that `R` is a + relation defined for `T`. +- Given an `IncludesCheck` as `this.related.R.includes(ctx.subject)`, we check + that + - `R` is a relation defined for the current namespace. +- Given a `TransitiveCheck` as + `this.related.R.transitive(x = x.permits.P(ctx.subject))`, we check that + - `R` is a relation defined for the current namespace and that + - `P` is a permission defined for all types referenced by `R`. +- Given a `TransitiveCheck` as + `this.related.R.transitive(x = x.related.S.includes(ctx.subject))`, we check + that + - `R` is a relation defined for the current namespace and that + - `S` is a relation defined for all types referenced by `R`. + +## Examples + +The config can be type-checked in `strict` mode by TypeScript with the +[noLib](https://www.typescriptlang.org/tsconfig#noLib) option (preventing the +standard globals), and the +[strictPropertyInitialization](https://www.typescriptlang.org/tsconfig#strictPropertyInitialization) +option (allowing uninitialized properties). + +```ts +class User implements Namespace { + related: { + manager: User[] + } +} + +class Group implements Namespace { + related: { + members: (User | Group)[] + } +} + +class Folder implements Namespace { + related: { + parents: File[] + viewers: (User | SubjectSet)[] + } + + permits = { + view: (ctx: Context): boolean => this.related.viewers.includes(ctx.subject), + } +} + +class File implements Namespace { + related: { + parents: (File | Folder)[] + viewers: (User | SubjectSet)[] + owners: (User | SubjectSet)[] + siblings: File[] + } + + permits = { + view: (ctx: Context): boolean => + this.related.parents.traverse((p) => + p.related.viewers.includes(ctx.subject), + ) || + this.related.parents.traverse((p) => p.permits.view(ctx)) || + this.related.viewers.includes(ctx.subject) || + this.related.owners.includes(ctx.subject), + + edit: (ctx: Context) => this.related.owners.includes(ctx.subject), + + rename: (ctx: Context) => + this.related.siblings.traverse((s) => s.permits.edit(ctx)), + } +} +``` diff --git a/docs/sidebar.json b/docs/sidebar.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/docs/sidebar.json @@ -0,0 +1 @@ +[] diff --git a/embedx/config.schema.json b/embedx/config.schema.json new file mode 100644 index 0000000..f133756 --- /dev/null +++ b/embedx/config.schema.json @@ -0,0 +1,508 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/keto/master/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Keto Configuration", + "type": "object", + "definitions": { + "$schema": { + "type": "string", + "format": "uri-reference", + "description": "Add this to allow defining the schema, useful for IDE integration" + }, + "namespace": { + "type": "object", + "properties": { + "$schema": { + "$ref": "#/definitions/$schema" + }, + "name": { + "type": "string", + "title": "The name of the namespace.", + "examples": ["videos", "groups", "files"] + }, + "id": { + "type": "integer", + "title": "The unique ID of the namespace. Can not be changed once set.", + "minimum": 0, + "maximum": 2147483647 + }, + "config": { + "type": "object", + "title": "The configuration of the namespace.", + "description": "To be defined." + } + }, + "additionalProperties": false, + "required": ["name"] + }, + "tlsxSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + }, + "tlsx": { + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + } + } + }, + "cors": { + "title": "Cross Origin Resource Sharing (CORS)", + "description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "title": "Enable CORS", + "description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered." + }, + "allowed_origins": { + "title": "Allowed Origins", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "default": ["*"], + "uniqueItems": true, + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "title": "Allowed HTTP Methods", + "description": "A list of methods the client is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ] + }, + "uniqueItems": true, + "default": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "allowed_headers": { + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "title": "Allowed Request HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Authorization", "Content-Type"] + }, + "exposed_headers": { + "description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "title": "Allowed Response HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Content-Type"] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow HTTP Credentials", + "default": false, + "description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates." + }, + "max_age": { + "type": "integer", + "default": 0, + "title": "Maximum Age", + "description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age." + }, + "debug": { + "type": "boolean", + "default": false, + "title": "Enable Debugging", + "description": "Set to true to debug server side CORS issues." + } + }, + "additionalProperties": false + } + }, + "required": ["dsn"], + "additionalProperties": false, + "properties": { + "$schema": { + "$ref": "#/definitions/$schema" + }, + "dsn": { + "title": "Data Source Name", + "type": "string", + "description": "Sets the data source name. This configures the backend where ORY Keto persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.com/docs/hydra/dependencies-environment#sql", + "examples": [ + "postgres://user:password@host:123/database", + "mysql://user:password@tcp(host:123)/database", + "memory" + ] + }, + "serve": { + "type": "object", + "additionalProperties": false, + "properties": { + "read": { + "type": "object", + "title": "Read API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4466, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Read Listen File", + "description": "The path to a file that will be created when the read API is ready to accept connections. The content of the file is the host:port of the read API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-read-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "write": { + "type": "object", + "title": "Write API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4467, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Write Listen File", + "description": "The path to a file that will be created when the write API is ready to accept connections. The content of the file is the host:port of the write API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-write-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "metrics": { + "type": "object", + "title": "Metrics API (http only)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4468, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "Metrics Listen File", + "description": "The path to a file that will be created when the metrics API is ready to accept connections. The content of the file is the host:port of the metrics API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-metrics-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "opl": { + "type": "object", + "title": "Ory Permission Language Syntax API (http and gRPC)", + "additionalProperties": false, + "properties": { + "port": { + "type": "integer", + "default": 4469, + "title": "Port", + "description": "The port to listen on.", + "minimum": 0, + "maximum": 65535 + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "write_listen_file": { + "type": "string", + "title": "OPL Listen File", + "description": "The path to a file that will be created when the OPL API is ready to accept connections. The content of the file is the host:port of the OPL API. Use this to get the actual port when using port 0. The service might not yet be ready to accept connections when the file is created.", + "format": "uri", + "examples": ["file:///tmp/keto-opl-api"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + } + } + }, + "profiling": { + "title": "Profiling", + "description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "type": "string", + "enum": ["cpu", "mem", ""] + }, + "log": { + "$ref": "ory://logging-config" + }, + "tracing": { + "$ref": "ory://tracing-config" + }, + "namespaces": { + "description": "Namespace configuration or it's location.", + "default": "file://./keto_namespaces", + "oneOf": [ + { + "title": "Legacy namespace Repo URI", + "description": "A URI that points to a directory of namespace files, or a single file with all namespaces", + "type": "string", + "format": "uri" + }, + { + "type": "array", + "title": "Legacy namespace configuration", + "items": { + "$ref": "#/definitions/namespace" + } + }, + { + "type": "object", + "title": "Namespace configuration", + "properties": { + "location": { + "type": "string", + "title": "Ory Permission Language config file URI", + "description": "A URI that points to a directory of namespace files, or a single file with all namespaces", + "format": "uri", + "examples": [ + "file://./keto_namespaces.ts", + "file:///etc/configs/keto_namespaces.ts" + ] + }, + "experimental_strict_mode": { + "type": "boolean", + "title": "Strict permission checking mode (deprecated)", + "description": "Deprecated: use feature_flags.strict_mode instead. If strict mode is enabled, then relation tuples for permits are not checked directly (but the rewrites are applied). Similarly, subject sets are only expanded if they were declared with SubjectSet<...>. These stricter rules result in much faster checks with fewer queries to the underlying database." + } + }, + "additionalProperties": false, + "required": ["location"] + } + ] + }, + "limit": { + "type": "object", + "title": "Limits", + "description": "Limits aiming to control the resource consumption. These limits are not a sufficient replacement for rate-limiting.", + "properties": { + "max_read_depth": { + "type": "integer", + "default": 5, + "title": "Global maximum read depth", + "description": "The global maximum depth on all read operations. Note that this does not affect how deeply nested the tuples can be. This value can be decreased for a request by a value specified on the request, only if the request-specific value is greater than 1 and less than the global maximum depth.", + "minimum": 1, + "maximum": 65535 + }, + "max_read_width": { + "type": "integer", + "default": 100, + "title": "Global maximum read width", + "description": "The global maximum width on all read operations. Note that this does not affect how deeply nested the tuples can be. This value can be decreased for a request by a value specified on the request, only if the request-specific value is greater than 1 and less than the global maximum width.", + "minimum": 1, + "maximum": 65535 + }, + "max_batch_check_size": { + "type": "integer", + "default": 10, + "title": "Maximum batch check size", + "description": "The maximum number of tuples that will be accepted by the batch check endpoint.", + "minimum": 1, + "maximum": 65535 + }, + "batch_check_max_parallelization": { + "type": "integer", + "default": 5, + "title": "Max concurrent checks during batch check", + "description": "The limit for the number of tuples that will be checked concurrently during a batch check.", + "minimum": 1, + "maximum": 65535 + }, + "max_expand_size": { + "type": "integer", + "default": 4000, + "title": "Maximum tuples during expand", + "description": "The maximum total number of relation tuples the expand engine will return in a single BuildTree call. Once the limit is reached, further nodes are returned as truncated.", + "minimum": 0, + "maximum": 65535 + } + }, + "additionalProperties": false + }, + "clients": { + "title": "Global outgoing network settings", + "description": "Configure how outgoing network calls behave.", + "type": "object", + "properties": { + "http": { + "title": "Global HTTP client configuration", + "description": "Configure how outgoing HTTP calls behave.", + "type": "object", + "properties": { + "disallow_private_ip_ranges": { + "title": "Disallow private IP ranges", + "description": "Disallow all outgoing HTTP calls to private IP ranges. This feature can help protect against SSRF attacks.", + "type": "boolean", + "default": false + } + } + } + } + }, + "secrets": { + "title": "Secrets for signing and encryption", + "description": "Make sure to use unique and random secrets for production deployments. Horizontally scaled deployments have to use the same secrets across all instances.", + "type": "object", + "properties": { + "pagination": { + "description": "Pagination secrets are used to encrypt pagination tokens. The first secret is used for encryption, while all others are used for decryption. This allows you to rotate the encryption key without invalidating all existing pagination tokens.", + "type": "array", + "minLength": 1, + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "secret used for encryption", + "old secret kept for decryption", + "another old secret kept for decryption" + ] + ] + } + } + }, + "feature_flags": { + "title": "Feature flags", + "description": "Opt in to features that are still being refined. Behavior may change in future releases.", + "type": "object", + "additionalProperties": false, + "properties": { + "expand_rewrites": { + "type": "boolean", + "title": "Expand OPL rewrite rules", + "description": "If enabled, the expand API follows OPL rewrite rules when building permission trees.", + "default": false + }, + "strict_mode": { + "type": "boolean", + "title": "Strict permission checking mode", + "description": "If strict mode is enabled, then relation tuples for permits are not checked directly (but the rewrites are applied). Similarly, subject sets are only expanded if they were declared with SubjectSet<...>. These stricter rules result in much faster checks with fewer queries to the underlying database.", + "default": false + } + } + }, + "version": { + "type": "string", + "title": "The Keto version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + } + } +} diff --git a/embedx/schemas.go b/embedx/schemas.go new file mode 100644 index 0000000..8bc6074 --- /dev/null +++ b/embedx/schemas.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package embedx + +import ( + "bytes" + _ "embed" + "io" + + "github.com/gofrs/uuid" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/pkg/errors" + "github.com/tidwall/gjson" +) + +//go:embed config.schema.json +var ConfigSchema []byte + +var ConfigSchemaID string + +func init() { + ConfigSchemaID = gjson.GetBytes(ConfigSchema, "$id").String() + if ConfigSchemaID == "" { + ConfigSchemaID = uuid.Must(uuid.NewV4()).String() + ".json" + } +} + +// AddConfigSchema should be used instead of the schema itself to auto-register the dependencies schemas. +func AddConfigSchema(compiler interface { + AddResource(url string, r io.Reader) error +}) error { + if err := otelx.AddConfigSchema(compiler); err != nil { + return err + } + if err := logrusx.AddConfigSchema(compiler); err != nil { + return err + } + + return errors.WithStack(compiler.AddResource(ConfigSchemaID, bytes.NewReader(ConfigSchema))) +} diff --git a/embedx/schemas_test.go b/embedx/schemas_test.go new file mode 100644 index 0000000..d4b5788 --- /dev/null +++ b/embedx/schemas_test.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package embedx + +import ( + "context" + "testing" + + "github.com/ory/jsonschema/v3" + "github.com/stretchr/testify/require" +) + +func TestConfigSchema(t *testing.T) { + c := jsonschema.NewCompiler() + require.NoError(t, AddConfigSchema(c)) + + _, err := c.Compile(context.Background(), ConfigSchemaID) + require.NoError(t, err) +} diff --git a/gen/doc/buf.md b/gen/doc/buf.md new file mode 100644 index 0000000..2fd29f7 --- /dev/null +++ b/gen/doc/buf.md @@ -0,0 +1,652 @@ + + + +# Protocol Documentation + + + +## Table of Contents + +- [ory/keto/opl/v1alpha1/syntax_service.proto](#ory_keto_opl_v1alpha1_syntax_service-proto) + - [CheckRequest](#ory-keto-opl-v1alpha1-CheckRequest) + - [CheckResponse](#ory-keto-opl-v1alpha1-CheckResponse) + - [ParseError](#ory-keto-opl-v1alpha1-ParseError) + - [SourcePosition](#ory-keto-opl-v1alpha1-SourcePosition) + - [SyntaxService](#ory-keto-opl-v1alpha1-SyntaxService) + +- [ory/keto/relation_tuples/v1alpha2/relation_tuples.proto](#ory_keto_relation_tuples_v1alpha2_relation_tuples-proto) + - [RelationQuery](#ory-keto-relation_tuples-v1alpha2-RelationQuery) + - [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) + - [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) + - [SubjectSet](#ory-keto-relation_tuples-v1alpha2-SubjectSet) +- [ory/keto/relation_tuples/v1alpha2/check_service.proto](#ory_keto_relation_tuples_v1alpha2_check_service-proto) + - [BatchCheckRequest](#ory-keto-relation_tuples-v1alpha2-BatchCheckRequest) + - [BatchCheckResponse](#ory-keto-relation_tuples-v1alpha2-BatchCheckResponse) + - [CheckRequest](#ory-keto-relation_tuples-v1alpha2-CheckRequest) + - [CheckResponse](#ory-keto-relation_tuples-v1alpha2-CheckResponse) + - [CheckResponseWithError](#ory-keto-relation_tuples-v1alpha2-CheckResponseWithError) + - [CheckService](#ory-keto-relation_tuples-v1alpha2-CheckService) + +- [ory/keto/relation_tuples/v1alpha2/expand_service.proto](#ory_keto_relation_tuples_v1alpha2_expand_service-proto) + - [ExpandRequest](#ory-keto-relation_tuples-v1alpha2-ExpandRequest) + - [ExpandResponse](#ory-keto-relation_tuples-v1alpha2-ExpandResponse) + - [SubjectTree](#ory-keto-relation_tuples-v1alpha2-SubjectTree) + - [NodeType](#ory-keto-relation_tuples-v1alpha2-NodeType) + - [ExpandService](#ory-keto-relation_tuples-v1alpha2-ExpandService) + +- [ory/keto/relation_tuples/v1alpha2/namespaces_service.proto](#ory_keto_relation_tuples_v1alpha2_namespaces_service-proto) + - [ListNamespacesRequest](#ory-keto-relation_tuples-v1alpha2-ListNamespacesRequest) + - [ListNamespacesResponse](#ory-keto-relation_tuples-v1alpha2-ListNamespacesResponse) + - [Namespace](#ory-keto-relation_tuples-v1alpha2-Namespace) + - [NamespacesService](#ory-keto-relation_tuples-v1alpha2-NamespacesService) + +- [ory/keto/relation_tuples/v1alpha2/read_service.proto](#ory_keto_relation_tuples_v1alpha2_read_service-proto) + - [ListRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesRequest) + - [ListRelationTuplesRequest.Query](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesRequest-Query) + - [ListRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesResponse) + - [ReadService](#ory-keto-relation_tuples-v1alpha2-ReadService) + +- [ory/keto/relation_tuples/v1alpha2/version.proto](#ory_keto_relation_tuples_v1alpha2_version-proto) + - [GetVersionRequest](#ory-keto-relation_tuples-v1alpha2-GetVersionRequest) + - [GetVersionResponse](#ory-keto-relation_tuples-v1alpha2-GetVersionResponse) + - [VersionService](#ory-keto-relation_tuples-v1alpha2-VersionService) + +- [ory/keto/relation_tuples/v1alpha2/write_service.proto](#ory_keto_relation_tuples_v1alpha2_write_service-proto) + - [DeleteRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesRequest) + - [DeleteRelationTuplesRequest.Query](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesRequest-Query) + - [DeleteRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesResponse) + - [RelationTupleDelta](#ory-keto-relation_tuples-v1alpha2-RelationTupleDelta) + - [TransactRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-TransactRelationTuplesRequest) + - [TransactRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-TransactRelationTuplesResponse) + - [RelationTupleDelta.Action](#ory-keto-relation_tuples-v1alpha2-RelationTupleDelta-Action) + - [WriteService](#ory-keto-relation_tuples-v1alpha2-WriteService) + +- [Scalar Value Types](#scalar-value-types) + + + +

Top

+ +## ory/keto/opl/v1alpha1/syntax_service.proto + + + +### CheckRequest + +| Field | Type | Label | Description | +| ------- | --------------- | ----- | ----------- | +| content | [bytes](#bytes) | | | + + + +### CheckResponse + +| Field | Type | Label | Description | +| ------------ | ----------------------------------------------- | -------- | ----------- | +| parse_errors | [ParseError](#ory-keto-opl-v1alpha1-ParseError) | repeated | | + + + +### ParseError + +| Field | Type | Label | Description | +| ------- | ------------------------------------------------------- | ----- | ----------- | +| message | [string](#string) | | | +| start | [SourcePosition](#ory-keto-opl-v1alpha1-SourcePosition) | | | +| end | [SourcePosition](#ory-keto-opl-v1alpha1-SourcePosition) | | | + + + +### SourcePosition + +| Field | Type | Label | Description | +| ------ | ----------------- | ----- | ----------- | +| line | [uint32](#uint32) | | | +| column | [uint32](#uint32) | | | + + + + + + + + + +### SyntaxService + +The service that checks the syntax of an OPL file. + +| Method Name | Request Type | Response Type | Description | +| ----------- | --------------------------------------------------- | ----------------------------------------------------- | -------------------------------- | +| Check | [CheckRequest](#ory-keto-opl-v1alpha1-CheckRequest) | [CheckResponse](#ory-keto-opl-v1alpha1-CheckResponse) | Performs a syntax check request. | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/relation_tuples.proto + + + +### RelationQuery + +The query for listing relationships. Clients can specify any optional field to +partially filter for specific relationships. + +Example use cases (namespace is always required): + +- object only: display a list of all permissions referring to a specific object +- relation only: get all groups that have members; get all directories that have + content +- object & relation: display all subjects that have a specific permission + relation +- subject & relation: display all groups a subject belongs to; display all + objects a subject has access to +- object & relation & subject: check whether the relation tuple already exists + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| namespace | [string](#string) | optional | The namespace this relation tuple lives in. | +| object | [string](#string) | optional | The object related by this tuple.
It is an object in the namespace of the tuple. | +| relation | [string](#string) | optional | The relation between an Object and a Subject. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | optional | The subject related by this tuple.
A Subject either represents a concrete subject id or
a `SubjectSet` that expands to more Subjects. | + + + +### RelationTuple + +RelationTuple defines a relation between an Object and a Subject. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| namespace | [string](#string) | | The namespace this relation tuple lives in. | +| object | [string](#string) | | The object related by this tuple.
It is an object in the namespace of the tuple. | +| relation | [string](#string) | | The relation between an Object and a Subject. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | The subject related by this tuple.
A Subject either represents a concrete subject id or
a `SubjectSet` that expands to more Subjects. | + + + +### Subject + +Subject is either a concrete subject id or a `SubjectSet` expanding to more +Subjects. + +| Field | Type | Label | Description | +| ----- | ----------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------ | +| id | [string](#string) | | A concrete id of the subject. | +| set | [SubjectSet](#ory-keto-relation_tuples-v1alpha2-SubjectSet) | | A subject set that expands to more Subjects.
More information are available under [concepts](../concepts/15_subjects.mdx). | + + + +### SubjectSet + +SubjectSet refers to all subjects who have the same `relation` on an `object`. + +| Field | Type | Label | Description | +| --------- | ----------------- | ----- | ---------------------------------------------------------------------------- | +| namespace | [string](#string) | | The namespace of the object and relation
referenced in this subject set. | +| object | [string](#string) | | The object related by this subject set. | +| relation | [string](#string) | | The relation between the object and the subjects. | + + + + + + + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/check_service.proto + + + +### BatchCheckRequest + +The request for a CheckService.BatchCheck RPC. Checks a batch of relations. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| tuples | [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) | repeated | | +| latest | [bool](#bool) | | This field is not implemented yet and has no effect.
| +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| +| max_depth | [int32](#int32) | | The maximum depth to search for a relation.

If the value is less than 1 or greater than the global
max-depth then the global max-depth will be used instead. | + + + +### BatchCheckResponse + +The response for a CheckService.BatchCheck rpc. + +| Field | Type | Label | Description | +| ------- | ----------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------- | +| results | [CheckResponseWithError](#ory-keto-relation_tuples-v1alpha2-CheckResponseWithError) | repeated | The results of the batch check. The order of these
results will match the order of the input. | + + + +### CheckRequest + +The request for a CheckService.Check RPC. Checks whether a specific subject is +related to an object. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| namespace | [string](#string) | | **Deprecated.** The namespace to evaluate the check.

Note: If you use the expand-API and the check
evaluates a RelationTuple specifying a SubjectSet as
subject or due to a rewrite rule in a namespace config
this check request may involve other namespaces automatically. | +| object | [string](#string) | | **Deprecated.** The related object in this check. | +| relation | [string](#string) | | **Deprecated.** The relation between the Object and the Subject. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | **Deprecated.** The related subject in this check. | +| tuple | [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) | | | +| latest | [bool](#bool) | | This field is not implemented yet and has no effect.
| +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| +| max_depth | [int32](#int32) | | The maximum depth to search for a relation.

If the value is less than 1 or greater than the global
max-depth then the global max-depth will be used instead. | + + + +### CheckResponse + +The response for a CheckService.Check rpc. + +| Field | Type | Label | Description | +| --------- | ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allowed | [bool](#bool) | | Whether the specified subject (id)
is related to the requested object.

It is false by default if no ACL matches. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| + + + +### CheckResponseWithError + +The response for an individual check in the CheckService.BatchCheck rpc. + +| Field | Type | Label | Description | +| --------- | ----------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| allowed | [bool](#bool) | | Whether the specified subject (id)
is related to the requested object.

It is false by default if no ACL matches. | +| error | [string](#string) | | If there was an error checking the tuple,
this will contain the error message.

If the check was performed successfully, this will be empty. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| + + + + + + + + + +### CheckService + +The service that performs authorization checks based on the stored Access +Control Lists. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- | -------------------------------- | +| Check | [CheckRequest](#ory-keto-relation_tuples-v1alpha2-CheckRequest) | [CheckResponse](#ory-keto-relation_tuples-v1alpha2-CheckResponse) | Performs an authorization check. | +| BatchCheck | [BatchCheckRequest](#ory-keto-relation_tuples-v1alpha2-BatchCheckRequest) | [BatchCheckResponse](#ory-keto-relation_tuples-v1alpha2-BatchCheckResponse) | | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/expand_service.proto + + + +### ExpandRequest + +The request for an ExpandService.Expand RPC. Expands the given subject set. + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | The subject to expand. | +| max_depth | [int32](#int32) | | The maximum depth of tree to build.

If the value is less than 1 or greater than the global
max-depth then the global max-depth will be used instead.

It is important to set this parameter to a meaningful
value. Ponder how deep you really want to display this. | +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| + + + +### ExpandResponse + +The response for a ExpandService.Expand RPC. + +| Field | Type | Label | Description | +| ----- | ------------------------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| tree | [SubjectTree](#ory-keto-relation_tuples-v1alpha2-SubjectTree) | | The tree the requested subject set expands to.
The requested subject set is the subject of the root.

This field can be nil in some circumstances. | + + + +### SubjectTree + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------- | +| node_type | [NodeType](#ory-keto-relation_tuples-v1alpha2-NodeType) | | The type of the node. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | **Deprecated.** The subject this node represents.
Deprecated: More information is now available in the tuple field. | +| tuple | [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) | | The relation tuple this node represents. | +| children | [SubjectTree](#ory-keto-relation_tuples-v1alpha2-SubjectTree) | repeated | The children of this node.

This is never set if `node_type` == `NODE_TYPE_LEAF`. | + + + + + +### NodeType + +| Name | Number | Description | +| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| NODE_TYPE_UNSPECIFIED | 0 | | +| NODE_TYPE_UNION | 1 | This node expands to a union of all children. | +| NODE_TYPE_EXCLUSION | 2 | Not implemented yet. | +| NODE_TYPE_INTERSECTION | 3 | Not implemented yet. | +| NODE_TYPE_LEAF | 4 | This node is a leaf and contains no children.
Its subject is a `SubjectID` unless `max_depth` was reached. | + + + + + + + +### ExpandService + +The service that performs subject set expansion based on the stored Access +Control Lists. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | ----------------------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------ | +| Expand | [ExpandRequest](#ory-keto-relation_tuples-v1alpha2-ExpandRequest) | [ExpandResponse](#ory-keto-relation_tuples-v1alpha2-ExpandResponse) | Expands the subject set into a tree of subjects. | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + + + +### ListNamespacesRequest + +Request for ReadService.ListNamespaces RPC. + + + +### ListNamespacesResponse + +| Field | Type | Label | Description | +| ---------- | --------------------------------------------------------- | -------- | ----------- | +| namespaces | [Namespace](#ory-keto-relation_tuples-v1alpha2-Namespace) | repeated | | + + + +### Namespace + +| Field | Type | Label | Description | +| ----- | ----------------- | ----- | ----------- | +| name | [string](#string) | | | + + + + + + + + + +### NamespacesService + +The service to query namespaces. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| -------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------- | +| ListNamespaces | [ListNamespacesRequest](#ory-keto-relation_tuples-v1alpha2-ListNamespacesRequest) | [ListNamespacesResponse](#ory-keto-relation_tuples-v1alpha2-ListNamespacesResponse) | Lists Namespaces | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/read_service.proto + + + +### ListRelationTuplesRequest + +Request for ReadService.ListRelationTuples RPC. See +`ListRelationTuplesRequest_Query` for how to filter the query. + +| Field | Type | Label | Description | +| -------------- | ----------------------------------------------------------------------------------------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| query | [ListRelationTuplesRequest.Query](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesRequest-Query) | | **Deprecated.** All query constraints are concatenated
with a logical AND operator.

The RelationTuple list from ListRelationTuplesResponse
is ordered from the newest RelationTuple to the oldest. | +| relation_query | [RelationQuery](#ory-keto-relation_tuples-v1alpha2-RelationQuery) | | | +| expand_mask | [google.protobuf.FieldMask](#google-protobuf-FieldMask) | | This field is not implemented yet and has no effect.
| +| snaptoken | [string](#string) | | This field is not implemented yet and has no effect.
| +| page_size | [int32](#int32) | | Optional. The maximum number of
RelationTuples to return in the response.

Default: 100 | +| page_token | [string](#string) | | Optional. An opaque pagination token returned from
a previous call to `ListRelationTuples` that
indicates where the page should start at.

An empty token denotes the first page. All successive
pages require the token from the previous page. | + + + +### ListRelationTuplesRequest.Query + +The query for listing relationships. Clients can specify any optional field to +partially filter for specific relationships. + +Example use cases (namespace is always required): + +- object only: display a list of all permissions referring to a specific object +- relation only: get all groups that have members; get all directories that have + content +- object & relation: display all subjects that have a specific permission + relation +- subject & relation: display all groups a subject belongs to; display all + objects a subject has access to +- object & relation & subject: check whether the relation tuple already exists + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------- | ----- | ------------------------------------ | +| namespace | [string](#string) | | Required. The namespace to query. | +| object | [string](#string) | | Optional. The object to query for. | +| relation | [string](#string) | | Optional. The relation to query for. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | Optional. The subject to query for. | + + + +### ListRelationTuplesResponse + +The response of a ReadService.ListRelationTuples RPC. + +| Field | Type | Label | Description | +| --------------- | ----------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------- | +| relation_tuples | [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) | repeated | The relationships matching the list request. | +| next_page_token | [string](#string) | | The token required to get the next page.
If this is the last page, the token will be the empty string. | + + + + + + + + + +### ReadService + +The service to query relationships. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis). + +| Method Name | Request Type | Response Type | Description | +| ------------------ | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------ | +| ListRelationTuples | [ListRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesRequest) | [ListRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-ListRelationTuplesResponse) | Lists ACL relationships. | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/version.proto + + + +### GetVersionRequest + +Request for the VersionService.GetVersion RPC. + + + +### GetVersionResponse + +Response of the VersionService.GetVersion RPC. + +| Field | Type | Label | Description | +| ------- | ----------------- | ----- | -------------------------------------------- | +| version | [string](#string) | | The version string of the Ory Keto instance. | + + + + + + + + + +### VersionService + +The service returning the specific Ory Keto instance version. + +This service is part of the +[read-APIs](../concepts/25_api-overview.mdx#read-apis) and +[write-APIs](../concepts/25_api-overview.mdx#write-apis). + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------- | +| GetVersion | [GetVersionRequest](#ory-keto-relation_tuples-v1alpha2-GetVersionRequest) | [GetVersionResponse](#ory-keto-relation_tuples-v1alpha2-GetVersionResponse) | Returns the version of the Ory Keto instance. | + + + + + +

Top

+ +## ory/keto/relation_tuples/v1alpha2/write_service.proto + + + +### DeleteRelationTuplesRequest + +| Field | Type | Label | Description | +| -------------- | --------------------------------------------------------------------------------------------------------- | ----- | --------------- | +| query | [DeleteRelationTuplesRequest.Query](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesRequest-Query) | | **Deprecated.** | +| relation_query | [RelationQuery](#ory-keto-relation_tuples-v1alpha2-RelationQuery) | | | + + + +### DeleteRelationTuplesRequest.Query + +The query for deleting relationships + +| Field | Type | Label | Description | +| --------- | ----------------------------------------------------- | ----- | ------------------------------------ | +| namespace | [string](#string) | | Optional. The namespace to query. | +| object | [string](#string) | | Optional. The object to query for. | +| relation | [string](#string) | | Optional. The relation to query for. | +| subject | [Subject](#ory-keto-relation_tuples-v1alpha2-Subject) | | Optional. The subject to query for. | + + + +### DeleteRelationTuplesResponse + + + +### RelationTupleDelta + +Write-delta for a TransactRelationTuplesRequest. + +| Field | Type | Label | Description | +| -------------- | ----------------------------------------------------------------------------------------- | ----- | -------------------------------------- | +| action | [RelationTupleDelta.Action](#ory-keto-relation_tuples-v1alpha2-RelationTupleDelta-Action) | | The action to do on the RelationTuple. | +| relation_tuple | [RelationTuple](#ory-keto-relation_tuples-v1alpha2-RelationTuple) | | The target RelationTuple. | + + + +### TransactRelationTuplesRequest + +The request of a WriteService.TransactRelationTuples RPC. + +| Field | Type | Label | Description | +| --------------------- | --------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| relation_tuple_deltas | [RelationTupleDelta](#ory-keto-relation_tuples-v1alpha2-RelationTupleDelta) | repeated | The write delta for the relationships operated in one single transaction.
Either all actions succeed or no change takes effect on error. | + + + +### TransactRelationTuplesResponse + +The response of a WriteService.TransactRelationTuples rpc. + +| Field | Type | Label | Description | +| ---------- | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| snaptokens | [string](#string) | repeated | This field is not implemented yet and has no effect.
| + + + + + +### RelationTupleDelta.Action + +| Name | Number | Description | +| ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------- | +| ACTION_UNSPECIFIED | 0 | Unspecified.
The `TransactRelationTuples` RPC ignores this
RelationTupleDelta if an action was unspecified. | +| ACTION_INSERT | 1 | Insertion of a new RelationTuple. | +| ACTION_DELETE | 2 | Deletion of the RelationTuple.
It is ignored if it does not exist. | + + + + + + + +### WriteService + +The write service to create and delete Access Control Lists. + +This service is part of the +[write-APIs](../concepts/25_api-overview.mdx#write-apis). + +| Method Name | Request Type | Response Type | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | +| TransactRelationTuples | [TransactRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-TransactRelationTuplesRequest) | [TransactRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-TransactRelationTuplesResponse) | Writes one or more relationships in a single transaction. | +| DeleteRelationTuples | [DeleteRelationTuplesRequest](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesRequest) | [DeleteRelationTuplesResponse](#ory-keto-relation_tuples-v1alpha2-DeleteRelationTuplesResponse) | Deletes relationships based on relation query | + + + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------- | ----------- | ------- | ---------- | -------------- | ------------------------------ | +| double | | double | double | float | float64 | double | float | Float | +| float | | float | float | float | float32 | float | float | Float | +| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) | +| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) | +| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) | +| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum | +| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) | +| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum | +| bool | | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | +| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) | +| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) | diff --git a/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.d.ts b/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.d.ts new file mode 100644 index 0000000..40ace4e --- /dev/null +++ b/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.d.ts @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/opl/v1alpha1/syntax_service.proto (package ory.keto.opl.v1alpha1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { CheckRequest, CheckResponse } from "./syntax_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that checks the syntax of an OPL file. + * + * @generated from service ory.keto.opl.v1alpha1.SyntaxService + */ +export declare const SyntaxService: { + readonly typeName: "ory.keto.opl.v1alpha1.SyntaxService" + readonly methods: { + /** + * Performs a syntax check request. + * + * @generated from rpc ory.keto.opl.v1alpha1.SyntaxService.Check + */ + readonly check: { + readonly name: "Check" + readonly I: typeof CheckRequest + readonly O: typeof CheckResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.js b/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.js new file mode 100644 index 0000000..b3963be --- /dev/null +++ b/gen/es/ory/keto/opl/v1alpha1/syntax_service_connect.js @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/opl/v1alpha1/syntax_service.proto (package ory.keto.opl.v1alpha1, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { CheckRequest, CheckResponse } from "./syntax_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that checks the syntax of an OPL file. + * + * @generated from service ory.keto.opl.v1alpha1.SyntaxService + */ +export const SyntaxService = { + typeName: "ory.keto.opl.v1alpha1.SyntaxService", + methods: { + /** + * Performs a syntax check request. + * + * @generated from rpc ory.keto.opl.v1alpha1.SyntaxService.Check + */ + check: { + name: "Check", + I: CheckRequest, + O: CheckResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.d.ts b/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.d.ts new file mode 100644 index 0000000..aa79b7e --- /dev/null +++ b/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.d.ts @@ -0,0 +1,118 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/opl/v1alpha1/syntax_service.proto (package ory.keto.opl.v1alpha1, syntax proto3) +/* eslint-disable */ + +import type { + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" + +/** + * Describes the file ory/keto/opl/v1alpha1/syntax_service.proto. + */ +export declare const file_ory_keto_opl_v1alpha1_syntax_service: GenFile + +/** + * @generated from message ory.keto.opl.v1alpha1.CheckRequest + */ +export declare type CheckRequest = + Message<"ory.keto.opl.v1alpha1.CheckRequest"> & { + /** + * @generated from field: bytes content = 1; + */ + content: Uint8Array + } + +/** + * Describes the message ory.keto.opl.v1alpha1.CheckRequest. + * Use `create(CheckRequestSchema)` to create a new message. + */ +export declare const CheckRequestSchema: GenMessage + +/** + * @generated from message ory.keto.opl.v1alpha1.CheckResponse + */ +export declare type CheckResponse = + Message<"ory.keto.opl.v1alpha1.CheckResponse"> & { + /** + * @generated from field: repeated ory.keto.opl.v1alpha1.ParseError parse_errors = 1; + */ + parseErrors: ParseError[] + } + +/** + * Describes the message ory.keto.opl.v1alpha1.CheckResponse. + * Use `create(CheckResponseSchema)` to create a new message. + */ +export declare const CheckResponseSchema: GenMessage + +/** + * @generated from message ory.keto.opl.v1alpha1.ParseError + */ +export declare type ParseError = Message<"ory.keto.opl.v1alpha1.ParseError"> & { + /** + * @generated from field: string message = 1; + */ + message: string + + /** + * @generated from field: ory.keto.opl.v1alpha1.SourcePosition start = 2; + */ + start?: SourcePosition + + /** + * @generated from field: ory.keto.opl.v1alpha1.SourcePosition end = 3; + */ + end?: SourcePosition +} + +/** + * Describes the message ory.keto.opl.v1alpha1.ParseError. + * Use `create(ParseErrorSchema)` to create a new message. + */ +export declare const ParseErrorSchema: GenMessage + +/** + * @generated from message ory.keto.opl.v1alpha1.SourcePosition + */ +export declare type SourcePosition = + Message<"ory.keto.opl.v1alpha1.SourcePosition"> & { + /** + * @generated from field: uint32 line = 1; + */ + line: number + + /** + * @generated from field: uint32 column = 2; + */ + column: number + } + +/** + * Describes the message ory.keto.opl.v1alpha1.SourcePosition. + * Use `create(SourcePositionSchema)` to create a new message. + */ +export declare const SourcePositionSchema: GenMessage + +/** + * The service that checks the syntax of an OPL file. + * + * @generated from service ory.keto.opl.v1alpha1.SyntaxService + */ +export declare const SyntaxService: GenService<{ + /** + * Performs a syntax check request. + * + * @generated from rpc ory.keto.opl.v1alpha1.SyntaxService.Check + */ + check: { + methodKind: "unary" + input: typeof CheckRequestSchema + output: typeof CheckResponseSchema + } +}> diff --git a/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.js b/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.js new file mode 100644 index 0000000..035708f --- /dev/null +++ b/gen/es/ory/keto/opl/v1alpha1/syntax_service_pb.js @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/opl/v1alpha1/syntax_service.proto (package ory.keto.opl.v1alpha1, syntax proto3) +/* eslint-disable */ + +import { + fileDesc, + messageDesc, + serviceDesc, +} from "@bufbuild/protobuf/codegenv2" + +/** + * Describes the file ory/keto/opl/v1alpha1/syntax_service.proto. + */ +export const file_ory_keto_opl_v1alpha1_syntax_service = + /*@__PURE__*/ + fileDesc( + "Cipvcnkva2V0by9vcGwvdjFhbHBoYTEvc3ludGF4X3NlcnZpY2UucHJvdG8SFW9yeS5rZXRvLm9wbC52MWFscGhhMSIfCgxDaGVja1JlcXVlc3QSDwoHY29udGVudBgBIAEoDCJICg1DaGVja1Jlc3BvbnNlEjcKDHBhcnNlX2Vycm9ycxgBIAMoCzIhLm9yeS5rZXRvLm9wbC52MWFscGhhMS5QYXJzZUVycm9yIocBCgpQYXJzZUVycm9yEg8KB21lc3NhZ2UYASABKAkSNAoFc3RhcnQYAiABKAsyJS5vcnkua2V0by5vcGwudjFhbHBoYTEuU291cmNlUG9zaXRpb24SMgoDZW5kGAMgASgLMiUub3J5LmtldG8ub3BsLnYxYWxwaGExLlNvdXJjZVBvc2l0aW9uIi4KDlNvdXJjZVBvc2l0aW9uEgwKBGxpbmUYASABKA0SDgoGY29sdW1uGAIgASgNMmMKDVN5bnRheFNlcnZpY2USUgoFQ2hlY2sSIy5vcnkua2V0by5vcGwudjFhbHBoYTEuQ2hlY2tSZXF1ZXN0GiQub3J5LmtldG8ub3BsLnYxYWxwaGExLkNoZWNrUmVzcG9uc2VC3AEKGWNvbS5vcnkua2V0by5vcGwudjFhbHBoYTFCElN5bnRheFNlcnZpY2VQcm90b1ABWjRnaXRodWIuY29tL29yeS9rZXRvL2dlbi9nby9vcnkva2V0by9vcGwvdjFhbHBoYTE7b3BsogIDT0tPqgIVT3J5LktldG8uT3BsLlYxYWxwaGExygIVT3J5XEtldG9cT3BsXFYxYWxwaGEx4gIhT3J5XEtldG9cT3BsXFYxYWxwaGExXEdQQk1ldGFkYXRh6gIYT3J5OjpLZXRvOjpPcGw6OlYxYWxwaGExYgZwcm90bzM", + ) + +/** + * Describes the message ory.keto.opl.v1alpha1.CheckRequest. + * Use `create(CheckRequestSchema)` to create a new message. + */ +export const CheckRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_opl_v1alpha1_syntax_service, 0) + +/** + * Describes the message ory.keto.opl.v1alpha1.CheckResponse. + * Use `create(CheckResponseSchema)` to create a new message. + */ +export const CheckResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_opl_v1alpha1_syntax_service, 1) + +/** + * Describes the message ory.keto.opl.v1alpha1.ParseError. + * Use `create(ParseErrorSchema)` to create a new message. + */ +export const ParseErrorSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_opl_v1alpha1_syntax_service, 2) + +/** + * Describes the message ory.keto.opl.v1alpha1.SourcePosition. + * Use `create(SourcePositionSchema)` to create a new message. + */ +export const SourcePositionSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_opl_v1alpha1_syntax_service, 3) + +/** + * The service that checks the syntax of an OPL file. + * + * @generated from service ory.keto.opl.v1alpha1.SyntaxService + */ +export const SyntaxService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_opl_v1alpha1_syntax_service, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.d.ts new file mode 100644 index 0000000..5af202e --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.d.ts @@ -0,0 +1,49 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/check_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + BatchCheckRequest, + BatchCheckResponse, + CheckRequest, + CheckResponse, +} from "./check_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that performs authorization checks + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.CheckService + */ +export declare const CheckService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.CheckService" + readonly methods: { + /** + * Performs an authorization check. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.Check + */ + readonly check: { + readonly name: "Check" + readonly I: typeof CheckRequest + readonly O: typeof CheckResponse + readonly kind: MethodKind.Unary + } + /** + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck + */ + readonly batchCheck: { + readonly name: "BatchCheck" + readonly I: typeof BatchCheckRequest + readonly O: typeof BatchCheckResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.js new file mode 100644 index 0000000..33fb2df --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_connect.js @@ -0,0 +1,49 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/check_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + BatchCheckRequest, + BatchCheckResponse, + CheckRequest, + CheckResponse, +} from "./check_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that performs authorization checks + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.CheckService + */ +export const CheckService = { + typeName: "ory.keto.relation_tuples.v1alpha2.CheckService", + methods: { + /** + * Performs an authorization check. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.Check + */ + check: { + name: "Check", + I: CheckRequest, + O: CheckResponse, + kind: MethodKind.Unary, + }, + /** + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck + */ + batchCheck: { + name: "BatchCheck", + I: BatchCheckRequest, + O: BatchCheckResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.d.ts new file mode 100644 index 0000000..ced39d7 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.d.ts @@ -0,0 +1,357 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/check_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" +import type { RelationTuple, Subject } from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/check_service.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_check_service: GenFile + +/** + * The request for a CheckService.Check RPC. + * Checks whether a specific subject is related to an object. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.CheckRequest + */ +export declare type CheckRequest = + Message<"ory.keto.relation_tuples.v1alpha2.CheckRequest"> & { + /** + * The namespace to evaluate the check. + * + * Note: If you use the expand-API and the check + * evaluates a RelationTuple specifying a SubjectSet as + * subject or due to a rewrite rule in a namespace config + * this check request may involve other namespaces automatically. + * + * @generated from field: string namespace = 1 [deprecated = true]; + * @deprecated + */ + namespace: string + + /** + * The related object in this check. + * + * @generated from field: string object = 2 [deprecated = true]; + * @deprecated + */ + object: string + + /** + * The relation between the Object and the Subject. + * + * @generated from field: string relation = 3 [deprecated = true]; + * @deprecated + */ + relation: string + + /** + * The related subject in this check. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 4 [deprecated = true]; + * @deprecated + */ + subject?: Subject + + /** + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationTuple tuple = 8; + */ + tuple?: RelationTuple + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: bool latest = 5; + */ + latest: boolean + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 6; + */ + snaptoken: string + + /** + * The maximum depth to search for a relation. + * + * If the value is less than 1 or greater than the global + * max-depth then the global max-depth will be used instead. + * + * @generated from field: int32 max_depth = 7; + */ + maxDepth: number + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckRequest. + * Use `create(CheckRequestSchema)` to create a new message. + */ +export declare const CheckRequestSchema: GenMessage + +/** + * The response for a CheckService.Check rpc. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.CheckResponse + */ +export declare type CheckResponse = + Message<"ory.keto.relation_tuples.v1alpha2.CheckResponse"> & { + /** + * Whether the specified subject (id) + * is related to the requested object. + * + * It is false by default if no ACL matches. + * + * @generated from field: bool allowed = 1; + */ + allowed: boolean + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 2; + */ + snaptoken: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckResponse. + * Use `create(CheckResponseSchema)` to create a new message. + */ +export declare const CheckResponseSchema: GenMessage + +/** + * The response for an individual check in the CheckService.BatchCheck rpc. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.CheckResponseWithError + */ +export declare type CheckResponseWithError = + Message<"ory.keto.relation_tuples.v1alpha2.CheckResponseWithError"> & { + /** + * Whether the specified subject (id) + * is related to the requested object. + * + * It is false by default if no ACL matches. + * + * @generated from field: bool allowed = 1; + */ + allowed: boolean + + /** + * If there was an error checking the tuple, + * this will contain the error message. + * + * If the check was performed successfully, this will be empty. + * + * @generated from field: string error = 2; + */ + error: string + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 3; + */ + snaptoken: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckResponseWithError. + * Use `create(CheckResponseWithErrorSchema)` to create a new message. + */ +export declare const CheckResponseWithErrorSchema: GenMessage + +/** + * The request for a CheckService.BatchCheck RPC. + * Checks a batch of relations. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.BatchCheckRequest + */ +export declare type BatchCheckRequest = + Message<"ory.keto.relation_tuples.v1alpha2.BatchCheckRequest"> & { + /** + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.RelationTuple tuples = 1; + */ + tuples: RelationTuple[] + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: bool latest = 2; + */ + latest: boolean + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 3; + */ + snaptoken: string + + /** + * The maximum depth to search for a relation. + * + * If the value is less than 1 or greater than the global + * max-depth then the global max-depth will be used instead. + * + * @generated from field: int32 max_depth = 4; + */ + maxDepth: number + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.BatchCheckRequest. + * Use `create(BatchCheckRequestSchema)` to create a new message. + */ +export declare const BatchCheckRequestSchema: GenMessage + +/** + * The response for a CheckService.BatchCheck rpc. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.BatchCheckResponse + */ +export declare type BatchCheckResponse = + Message<"ory.keto.relation_tuples.v1alpha2.BatchCheckResponse"> & { + /** + * The results of the batch check. The order of these + * results will match the order of the input. + * + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.CheckResponseWithError results = 1; + */ + results: CheckResponseWithError[] + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.BatchCheckResponse. + * Use `create(BatchCheckResponseSchema)` to create a new message. + */ +export declare const BatchCheckResponseSchema: GenMessage + +/** + * The service that performs authorization checks + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.CheckService + */ +export declare const CheckService: GenService<{ + /** + * Performs an authorization check. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.Check + */ + check: { + methodKind: "unary" + input: typeof CheckRequestSchema + output: typeof CheckResponseSchema + } + /** + * @generated from rpc ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck + */ + batchCheck: { + methodKind: "unary" + input: typeof BatchCheckRequestSchema + output: typeof BatchCheckResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.js new file mode 100644 index 0000000..3fa8b00 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/check_service_pb.js @@ -0,0 +1,75 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/check_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + fileDesc, + messageDesc, + serviceDesc, +} from "@bufbuild/protobuf/codegenv2" +import { file_ory_keto_relation_tuples_v1alpha2_relation_tuples } from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/check_service.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_check_service = + /*@__PURE__*/ + fileDesc( + "CjVvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvY2hlY2tfc2VydmljZS5wcm90bxIhb3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyIocCCgxDaGVja1JlcXVlc3QSFQoJbmFtZXNwYWNlGAEgASgJQgIYARISCgZvYmplY3QYAiABKAlCAhgBEhQKCHJlbGF0aW9uGAMgASgJQgIYARI/CgdzdWJqZWN0GAQgASgLMioub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlN1YmplY3RCAhgBEj8KBXR1cGxlGAggASgLMjAub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlJlbGF0aW9uVHVwbGUSDgoGbGF0ZXN0GAUgASgIEhEKCXNuYXB0b2tlbhgGIAEoCRIRCgltYXhfZGVwdGgYByABKAUiMwoNQ2hlY2tSZXNwb25zZRIPCgdhbGxvd2VkGAEgASgIEhEKCXNuYXB0b2tlbhgCIAEoCSJLChZDaGVja1Jlc3BvbnNlV2l0aEVycm9yEg8KB2FsbG93ZWQYASABKAgSDQoFZXJyb3IYAiABKAkSEQoJc25hcHRva2VuGAMgASgJIosBChFCYXRjaENoZWNrUmVxdWVzdBJACgZ0dXBsZXMYASADKAsyMC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuUmVsYXRpb25UdXBsZRIOCgZsYXRlc3QYAiABKAgSEQoJc25hcHRva2VuGAMgASgJEhEKCW1heF9kZXB0aBgEIAEoBSJgChJCYXRjaENoZWNrUmVzcG9uc2USSgoHcmVzdWx0cxgBIAMoCzI5Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5DaGVja1Jlc3BvbnNlV2l0aEVycm9yMvUBCgxDaGVja1NlcnZpY2USagoFQ2hlY2sSLy5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuQ2hlY2tSZXF1ZXN0GjAub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLkNoZWNrUmVzcG9uc2USeQoKQmF0Y2hDaGVjaxI0Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5CYXRjaENoZWNrUmVxdWVzdBo1Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5CYXRjaENoZWNrUmVzcG9uc2VCqgIKJWNvbS5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTJCEUNoZWNrU2VydmljZVByb3RvUAFaS2dpdGh1Yi5jb20vb3J5L2tldG8vZ2VuL2dvL29yeS9rZXRvL3JlbGF0aW9uX3R1cGxlcy92MWFscGhhMjtyZWxhdGlvbnR1cGxlc6ICA09LUqoCIE9yeS5LZXRvLlJlbGF0aW9uVHVwbGVzLlYxYWxwaGEyygIgT3J5XEtldG9cUmVsYXRpb25UdXBsZXNcVjFhbHBoYTLiAixPcnlcS2V0b1xSZWxhdGlvblR1cGxlc1xWMWFscGhhMlxHUEJNZXRhZGF0YeoCI09yeTo6S2V0bzo6UmVsYXRpb25UdXBsZXM6OlYxYWxwaGEyYgZwcm90bzM", + [file_ory_keto_relation_tuples_v1alpha2_relation_tuples], + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckRequest. + * Use `create(CheckRequestSchema)` to create a new message. + */ +export const CheckRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckResponse. + * Use `create(CheckResponseSchema)` to create a new message. + */ +export const CheckResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 1) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.CheckResponseWithError. + * Use `create(CheckResponseWithErrorSchema)` to create a new message. + */ +export const CheckResponseWithErrorSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 2) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.BatchCheckRequest. + * Use `create(BatchCheckRequestSchema)` to create a new message. + */ +export const BatchCheckRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 3) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.BatchCheckResponse. + * Use `create(BatchCheckResponseSchema)` to create a new message. + */ +export const BatchCheckResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 4) + +/** + * The service that performs authorization checks + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.CheckService + */ +export const CheckService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_check_service, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.d.ts new file mode 100644 index 0000000..40d4188 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.d.ts @@ -0,0 +1,35 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/expand_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { ExpandRequest, ExpandResponse } from "./expand_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that performs subject set expansion + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ExpandService + */ +export declare const ExpandService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.ExpandService" + readonly methods: { + /** + * Expands the subject set into a tree of subjects. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ExpandService.Expand + */ + readonly expand: { + readonly name: "Expand" + readonly I: typeof ExpandRequest + readonly O: typeof ExpandResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.js new file mode 100644 index 0000000..bdbf64e --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_connect.js @@ -0,0 +1,35 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/expand_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { ExpandRequest, ExpandResponse } from "./expand_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service that performs subject set expansion + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ExpandService + */ +export const ExpandService = { + typeName: "ory.keto.relation_tuples.v1alpha2.ExpandService", + methods: { + /** + * Expands the subject set into a tree of subjects. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ExpandService.Expand + */ + expand: { + name: "Expand", + I: ExpandRequest, + O: ExpandResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.d.ts new file mode 100644 index 0000000..96a1677 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.d.ts @@ -0,0 +1,210 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/expand_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenEnum, + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" +import type { RelationTuple, Subject } from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/expand_service.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_expand_service: GenFile + +/** + * The request for an ExpandService.Expand RPC. + * Expands the given subject set. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ExpandRequest + */ +export declare type ExpandRequest = + Message<"ory.keto.relation_tuples.v1alpha2.ExpandRequest"> & { + /** + * The subject to expand. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 1; + */ + subject?: Subject + + /** + * The maximum depth of tree to build. + * + * If the value is less than 1 or greater than the global + * max-depth then the global max-depth will be used instead. + * + * It is important to set this parameter to a meaningful + * value. Ponder how deep you really want to display this. + * + * @generated from field: int32 max_depth = 2; + */ + maxDepth: number + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 3; + */ + snaptoken: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ExpandRequest. + * Use `create(ExpandRequestSchema)` to create a new message. + */ +export declare const ExpandRequestSchema: GenMessage + +/** + * The response for a ExpandService.Expand RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ExpandResponse + */ +export declare type ExpandResponse = + Message<"ory.keto.relation_tuples.v1alpha2.ExpandResponse"> & { + /** + * The tree the requested subject set expands to. + * The requested subject set is the subject of the root. + * + * This field can be nil in some circumstances. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.SubjectTree tree = 1; + */ + tree?: SubjectTree + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ExpandResponse. + * Use `create(ExpandResponseSchema)` to create a new message. + */ +export declare const ExpandResponseSchema: GenMessage + +/** + * @generated from message ory.keto.relation_tuples.v1alpha2.SubjectTree + */ +export declare type SubjectTree = + Message<"ory.keto.relation_tuples.v1alpha2.SubjectTree"> & { + /** + * The type of the node. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.NodeType node_type = 1; + */ + nodeType: NodeType + + /** + * The subject this node represents. + * Deprecated: More information is now available in the tuple field. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 2 [deprecated = true]; + * @deprecated + */ + subject?: Subject + + /** + * The relation tuple this node represents. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationTuple tuple = 4; + */ + tuple?: RelationTuple + + /** + * The children of this node. + * + * This is never set if `node_type` == `NODE_TYPE_LEAF`. + * + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.SubjectTree children = 3; + */ + children: SubjectTree[] + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.SubjectTree. + * Use `create(SubjectTreeSchema)` to create a new message. + */ +export declare const SubjectTreeSchema: GenMessage + +/** + * @generated from enum ory.keto.relation_tuples.v1alpha2.NodeType + */ +export enum NodeType { + /** + * @generated from enum value: NODE_TYPE_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * This node expands to a union of all children. + * + * @generated from enum value: NODE_TYPE_UNION = 1; + */ + UNION = 1, + + /** + * Not implemented yet. + * + * @generated from enum value: NODE_TYPE_EXCLUSION = 2; + */ + EXCLUSION = 2, + + /** + * Not implemented yet. + * + * @generated from enum value: NODE_TYPE_INTERSECTION = 3; + */ + INTERSECTION = 3, + + /** + * This node is a leaf and contains no children. + * Its subject is a `SubjectID` unless `max_depth` was reached. + * + * @generated from enum value: NODE_TYPE_LEAF = 4; + */ + LEAF = 4, +} + +/** + * Describes the enum ory.keto.relation_tuples.v1alpha2.NodeType. + */ +export declare const NodeTypeSchema: GenEnum + +/** + * The service that performs subject set expansion + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ExpandService + */ +export declare const ExpandService: GenService<{ + /** + * Expands the subject set into a tree of subjects. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ExpandService.Expand + */ + expand: { + methodKind: "unary" + input: typeof ExpandRequestSchema + output: typeof ExpandResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.js new file mode 100644 index 0000000..b0aa61d --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/expand_service_pb.js @@ -0,0 +1,73 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/expand_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + enumDesc, + fileDesc, + messageDesc, + serviceDesc, + tsEnum, +} from "@bufbuild/protobuf/codegenv2" +import { file_ory_keto_relation_tuples_v1alpha2_relation_tuples } from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/expand_service.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_expand_service = + /*@__PURE__*/ + fileDesc( + "CjZvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvZXhwYW5kX3NlcnZpY2UucHJvdG8SIW9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMiJyCg1FeHBhbmRSZXF1ZXN0EjsKB3N1YmplY3QYASABKAsyKi5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuU3ViamVjdBIRCgltYXhfZGVwdGgYAiABKAUSEQoJc25hcHRva2VuGAMgASgJIk4KDkV4cGFuZFJlc3BvbnNlEjwKBHRyZWUYASABKAsyLi5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuU3ViamVjdFRyZWUikQIKC1N1YmplY3RUcmVlEj4KCW5vZGVfdHlwZRgBIAEoDjIrLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5Ob2RlVHlwZRI/CgdzdWJqZWN0GAIgASgLMioub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlN1YmplY3RCAhgBEj8KBXR1cGxlGAQgASgLMjAub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlJlbGF0aW9uVHVwbGUSQAoIY2hpbGRyZW4YAyADKAsyLi5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuU3ViamVjdFRyZWUqgwEKCE5vZGVUeXBlEhkKFU5PREVfVFlQRV9VTlNQRUNJRklFRBAAEhMKD05PREVfVFlQRV9VTklPThABEhcKE05PREVfVFlQRV9FWENMVVNJT04QAhIaChZOT0RFX1RZUEVfSU5URVJTRUNUSU9OEAMSEgoOTk9ERV9UWVBFX0xFQUYQBDJ+Cg1FeHBhbmRTZXJ2aWNlEm0KBkV4cGFuZBIwLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5FeHBhbmRSZXF1ZXN0GjEub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLkV4cGFuZFJlc3BvbnNlQqsCCiVjb20ub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyQhJFeHBhbmRTZXJ2aWNlUHJvdG9QAVpLZ2l0aHViLmNvbS9vcnkva2V0by9nZW4vZ28vb3J5L2tldG8vcmVsYXRpb25fdHVwbGVzL3YxYWxwaGEyO3JlbGF0aW9udHVwbGVzogIDT0tSqgIgT3J5LktldG8uUmVsYXRpb25UdXBsZXMuVjFhbHBoYTLKAiBPcnlcS2V0b1xSZWxhdGlvblR1cGxlc1xWMWFscGhhMuICLE9yeVxLZXRvXFJlbGF0aW9uVHVwbGVzXFYxYWxwaGEyXEdQQk1ldGFkYXRh6gIjT3J5OjpLZXRvOjpSZWxhdGlvblR1cGxlczo6VjFhbHBoYTJiBnByb3RvMw", + [file_ory_keto_relation_tuples_v1alpha2_relation_tuples], + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ExpandRequest. + * Use `create(ExpandRequestSchema)` to create a new message. + */ +export const ExpandRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_expand_service, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ExpandResponse. + * Use `create(ExpandResponseSchema)` to create a new message. + */ +export const ExpandResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_expand_service, 1) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.SubjectTree. + * Use `create(SubjectTreeSchema)` to create a new message. + */ +export const SubjectTreeSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_expand_service, 2) + +/** + * Describes the enum ory.keto.relation_tuples.v1alpha2.NodeType. + */ +export const NodeTypeSchema = + /*@__PURE__*/ + enumDesc(file_ory_keto_relation_tuples_v1alpha2_expand_service, 0) + +/** + * @generated from enum ory.keto.relation_tuples.v1alpha2.NodeType + */ +export const NodeType = /*@__PURE__*/ tsEnum(NodeTypeSchema) + +/** + * The service that performs subject set expansion + * based on the stored Access Control Lists. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ExpandService + */ +export const ExpandService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_expand_service, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.d.ts new file mode 100644 index 0000000..24bc115 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.d.ts @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + ListNamespacesRequest, + ListNamespacesResponse, +} from "./namespaces_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service to query namespaces. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.NamespacesService + */ +export declare const NamespacesService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.NamespacesService" + readonly methods: { + /** + * Lists Namespaces + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces + */ + readonly listNamespaces: { + readonly name: "ListNamespaces" + readonly I: typeof ListNamespacesRequest + readonly O: typeof ListNamespacesResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.js new file mode 100644 index 0000000..fe71071 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_connect.js @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + ListNamespacesRequest, + ListNamespacesResponse, +} from "./namespaces_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service to query namespaces. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.NamespacesService + */ +export const NamespacesService = { + typeName: "ory.keto.relation_tuples.v1alpha2.NamespacesService", + methods: { + /** + * Lists Namespaces + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces + */ + listNamespaces: { + name: "ListNamespaces", + I: ListNamespacesRequest, + O: ListNamespacesResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.d.ts new file mode 100644 index 0000000..ced890e --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.d.ts @@ -0,0 +1,86 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_namespaces_service: GenFile + +/** + * Request for ReadService.ListNamespaces RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest + */ +export declare type ListNamespacesRequest = + Message<"ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest"> & {} + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest. + * Use `create(ListNamespacesRequestSchema)` to create a new message. + */ +export declare const ListNamespacesRequestSchema: GenMessage + +/** + * @generated from message ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse + */ +export declare type ListNamespacesResponse = + Message<"ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse"> & { + /** + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.Namespace namespaces = 1; + */ + namespaces: Namespace[] + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse. + * Use `create(ListNamespacesResponseSchema)` to create a new message. + */ +export declare const ListNamespacesResponseSchema: GenMessage + +/** + * @generated from message ory.keto.relation_tuples.v1alpha2.Namespace + */ +export declare type Namespace = + Message<"ory.keto.relation_tuples.v1alpha2.Namespace"> & { + /** + * @generated from field: string name = 1; + */ + name: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.Namespace. + * Use `create(NamespaceSchema)` to create a new message. + */ +export declare const NamespaceSchema: GenMessage + +/** + * The service to query namespaces. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.NamespacesService + */ +export declare const NamespacesService: GenService<{ + /** + * Lists Namespaces + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces + */ + listNamespaces: { + methodKind: "unary" + input: typeof ListNamespacesRequestSchema + output: typeof ListNamespacesResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.js new file mode 100644 index 0000000..cf87e1a --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/namespaces_service_pb.js @@ -0,0 +1,56 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + fileDesc, + messageDesc, + serviceDesc, +} from "@bufbuild/protobuf/codegenv2" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/namespaces_service.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_namespaces_service = + /*@__PURE__*/ + fileDesc( + "Cjpvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvbmFtZXNwYWNlc19zZXJ2aWNlLnByb3RvEiFvcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIiFwoVTGlzdE5hbWVzcGFjZXNSZXF1ZXN0IloKFkxpc3ROYW1lc3BhY2VzUmVzcG9uc2USQAoKbmFtZXNwYWNlcxgBIAMoCzIsLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5OYW1lc3BhY2UiGQoJTmFtZXNwYWNlEgwKBG5hbWUYASABKAkymwEKEU5hbWVzcGFjZXNTZXJ2aWNlEoUBCg5MaXN0TmFtZXNwYWNlcxI4Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5MaXN0TmFtZXNwYWNlc1JlcXVlc3QaOS5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuTGlzdE5hbWVzcGFjZXNSZXNwb25zZUKvAgolY29tLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMkIWTmFtZXNwYWNlc1NlcnZpY2VQcm90b1ABWktnaXRodWIuY29tL29yeS9rZXRvL2dlbi9nby9vcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTI7cmVsYXRpb250dXBsZXOiAgNPS1KqAiBPcnkuS2V0by5SZWxhdGlvblR1cGxlcy5WMWFscGhhMsoCIE9yeVxLZXRvXFJlbGF0aW9uVHVwbGVzXFYxYWxwaGEy4gIsT3J5XEtldG9cUmVsYXRpb25UdXBsZXNcVjFhbHBoYTJcR1BCTWV0YWRhdGHqAiNPcnk6OktldG86OlJlbGF0aW9uVHVwbGVzOjpWMWFscGhhMmIGcHJvdG8z", + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest. + * Use `create(ListNamespacesRequestSchema)` to create a new message. + */ +export const ListNamespacesRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_namespaces_service, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse. + * Use `create(ListNamespacesResponseSchema)` to create a new message. + */ +export const ListNamespacesResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_namespaces_service, 1) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.Namespace. + * Use `create(NamespaceSchema)` to create a new message. + */ +export const NamespaceSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_namespaces_service, 2) + +/** + * The service to query namespaces. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.NamespacesService + */ +export const NamespacesService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_namespaces_service, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.d.ts new file mode 100644 index 0000000..3c2c732 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.d.ts @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/read_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + ListRelationTuplesRequest, + ListRelationTuplesResponse, +} from "./read_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service to query relationships. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ReadService + */ +export declare const ReadService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.ReadService" + readonly methods: { + /** + * Lists ACL relationships. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples + */ + readonly listRelationTuples: { + readonly name: "ListRelationTuples" + readonly I: typeof ListRelationTuplesRequest + readonly O: typeof ListRelationTuplesResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.js new file mode 100644 index 0000000..b38c0e1 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_connect.js @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/read_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + ListRelationTuplesRequest, + ListRelationTuplesResponse, +} from "./read_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service to query relationships. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ReadService + */ +export const ReadService = { + typeName: "ory.keto.relation_tuples.v1alpha2.ReadService", + methods: { + /** + * Lists ACL relationships. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples + */ + listRelationTuples: { + name: "ListRelationTuples", + I: ListRelationTuplesRequest, + O: ListRelationTuplesResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.d.ts new file mode 100644 index 0000000..99515df --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.d.ts @@ -0,0 +1,206 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/read_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" +import type { + RelationQuery, + RelationTuple, + Subject, +} from "./relation_tuples_pb" +import type { FieldMask } from "@bufbuild/protobuf/wkt" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/read_service.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_read_service: GenFile + +/** + * Request for ReadService.ListRelationTuples RPC. + * See `ListRelationTuplesRequest_Query` for how to filter the query. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest + */ +export declare type ListRelationTuplesRequest = + Message<"ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest"> & { + /** + * All query constraints are concatenated + * with a logical AND operator. + * + * The RelationTuple list from ListRelationTuplesResponse + * is ordered from the newest RelationTuple to the oldest. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query query = 1 [deprecated = true]; + * @deprecated + */ + query?: ListRelationTuplesRequest_Query + + /** + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationQuery relation_query = 6; + */ + relationQuery?: RelationQuery + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: google.protobuf.FieldMask expand_mask = 2; + */ + expandMask?: FieldMask + + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: string snaptoken = 3; + */ + snaptoken: string + + /** + * Optional. The maximum number of + * RelationTuples to return in the response. + * + * Default: 100 + * + * @generated from field: int32 page_size = 4; + */ + pageSize: number + + /** + * Optional. An opaque pagination token returned from + * a previous call to `ListRelationTuples` that + * indicates where the page should start at. + * + * An empty token denotes the first page. All successive + * pages require the token from the previous page. + * + * @generated from field: string page_token = 5; + */ + pageToken: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest. + * Use `create(ListRelationTuplesRequestSchema)` to create a new message. + */ +export declare const ListRelationTuplesRequestSchema: GenMessage + +/** + * The query for listing relationships. + * Clients can specify any optional field to + * partially filter for specific relationships. + * + * Example use cases (namespace is always required): + * - object only: display a list of all permissions referring to a specific object + * - relation only: get all groups that have members; get all directories that have content + * - object & relation: display all subjects that have a specific permission relation + * - subject & relation: display all groups a subject belongs to; display all objects a subject has access to + * - object & relation & subject: check whether the relation tuple already exists + * + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query + */ +export declare type ListRelationTuplesRequest_Query = + Message<"ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query"> & { + /** + * Required. The namespace to query. + * + * @generated from field: string namespace = 1; + */ + namespace: string + + /** + * Optional. The object to query for. + * + * @generated from field: string object = 2; + */ + object: string + + /** + * Optional. The relation to query for. + * + * @generated from field: string relation = 3; + */ + relation: string + + /** + * Optional. The subject to query for. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 4; + */ + subject?: Subject + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query. + * Use `create(ListRelationTuplesRequest_QuerySchema)` to create a new message. + */ +export declare const ListRelationTuplesRequest_QuerySchema: GenMessage + +/** + * The response of a ReadService.ListRelationTuples RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse + */ +export declare type ListRelationTuplesResponse = + Message<"ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse"> & { + /** + * The relationships matching the list request. + * + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.RelationTuple relation_tuples = 1; + */ + relationTuples: RelationTuple[] + + /** + * The token required to get the next page. + * If this is the last page, the token will be the empty string. + * + * @generated from field: string next_page_token = 2; + */ + nextPageToken: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse. + * Use `create(ListRelationTuplesResponseSchema)` to create a new message. + */ +export declare const ListRelationTuplesResponseSchema: GenMessage + +/** + * The service to query relationships. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ReadService + */ +export declare const ReadService: GenService<{ + /** + * Lists ACL relationships. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples + */ + listRelationTuples: { + methodKind: "unary" + input: typeof ListRelationTuplesRequestSchema + output: typeof ListRelationTuplesResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.js new file mode 100644 index 0000000..dfabf4f --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/read_service_pb.js @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/read_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + fileDesc, + messageDesc, + serviceDesc, +} from "@bufbuild/protobuf/codegenv2" +import { file_ory_keto_relation_tuples_v1alpha2_relation_tuples } from "./relation_tuples_pb" +import { file_google_protobuf_field_mask } from "@bufbuild/protobuf/wkt" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/read_service.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_read_service = + /*@__PURE__*/ + fileDesc( + "CjRvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvcmVhZF9zZXJ2aWNlLnByb3RvEiFvcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIiogMKGUxpc3RSZWxhdGlvblR1cGxlc1JlcXVlc3QSVQoFcXVlcnkYASABKAsyQi5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuTGlzdFJlbGF0aW9uVHVwbGVzUmVxdWVzdC5RdWVyeUICGAESSAoOcmVsYXRpb25fcXVlcnkYBiABKAsyMC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuUmVsYXRpb25RdWVyeRIvCgtleHBhbmRfbWFzaxgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5GaWVsZE1hc2sSEQoJc25hcHRva2VuGAMgASgJEhEKCXBhZ2Vfc2l6ZRgEIAEoBRISCgpwYWdlX3Rva2VuGAUgASgJGnkKBVF1ZXJ5EhEKCW5hbWVzcGFjZRgBIAEoCRIOCgZvYmplY3QYAiABKAkSEAoIcmVsYXRpb24YAyABKAkSOwoHc3ViamVjdBgEIAEoCzIqLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5TdWJqZWN0IoABChpMaXN0UmVsYXRpb25UdXBsZXNSZXNwb25zZRJJCg9yZWxhdGlvbl90dXBsZXMYASADKAsyMC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuUmVsYXRpb25UdXBsZRIXCg9uZXh0X3BhZ2VfdG9rZW4YAiABKAkyoQEKC1JlYWRTZXJ2aWNlEpEBChJMaXN0UmVsYXRpb25UdXBsZXMSPC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuTGlzdFJlbGF0aW9uVHVwbGVzUmVxdWVzdBo9Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5MaXN0UmVsYXRpb25UdXBsZXNSZXNwb25zZUKpAgolY29tLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMkIQUmVhZFNlcnZpY2VQcm90b1ABWktnaXRodWIuY29tL29yeS9rZXRvL2dlbi9nby9vcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTI7cmVsYXRpb250dXBsZXOiAgNPS1KqAiBPcnkuS2V0by5SZWxhdGlvblR1cGxlcy5WMWFscGhhMsoCIE9yeVxLZXRvXFJlbGF0aW9uVHVwbGVzXFYxYWxwaGEy4gIsT3J5XEtldG9cUmVsYXRpb25UdXBsZXNcVjFhbHBoYTJcR1BCTWV0YWRhdGHqAiNPcnk6OktldG86OlJlbGF0aW9uVHVwbGVzOjpWMWFscGhhMmIGcHJvdG8z", + [ + file_ory_keto_relation_tuples_v1alpha2_relation_tuples, + file_google_protobuf_field_mask, + ], + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest. + * Use `create(ListRelationTuplesRequestSchema)` to create a new message. + */ +export const ListRelationTuplesRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_read_service, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query. + * Use `create(ListRelationTuplesRequest_QuerySchema)` to create a new message. + */ +export const ListRelationTuplesRequest_QuerySchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_read_service, 0, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse. + * Use `create(ListRelationTuplesResponseSchema)` to create a new message. + */ +export const ListRelationTuplesResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_read_service, 1) + +/** + * The service to query relationships. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.ReadService + */ +export const ReadService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_read_service, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.d.ts new file mode 100644 index 0000000..fa9548d --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.d.ts @@ -0,0 +1,192 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/relation_tuples.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { GenFile, GenMessage } from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/relation_tuples.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_relation_tuples: GenFile + +/** + * RelationTuple defines a relation between an Object and a Subject. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.RelationTuple + */ +export declare type RelationTuple = + Message<"ory.keto.relation_tuples.v1alpha2.RelationTuple"> & { + /** + * The namespace this relation tuple lives in. + * + * @generated from field: string namespace = 1; + */ + namespace: string + + /** + * The object related by this tuple. + * It is an object in the namespace of the tuple. + * + * @generated from field: string object = 2; + */ + object: string + + /** + * The relation between an Object and a Subject. + * + * @generated from field: string relation = 3; + */ + relation: string + + /** + * The subject related by this tuple. + * A Subject either represents a concrete subject id or + * a `SubjectSet` that expands to more Subjects. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 4; + */ + subject?: Subject + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationTuple. + * Use `create(RelationTupleSchema)` to create a new message. + */ +export declare const RelationTupleSchema: GenMessage + +/** + * The query for listing relationships. + * Clients can specify any optional field to + * partially filter for specific relationships. + * + * Example use cases (namespace is always required): + * - object only: display a list of all permissions referring to a specific object + * - relation only: get all groups that have members; get all directories that have content + * - object & relation: display all subjects that have a specific permission relation + * - subject & relation: display all groups a subject belongs to; display all objects a subject has access to + * - object & relation & subject: check whether the relation tuple already exists + * + * @generated from message ory.keto.relation_tuples.v1alpha2.RelationQuery + */ +export declare type RelationQuery = + Message<"ory.keto.relation_tuples.v1alpha2.RelationQuery"> & { + /** + * The namespace this relation tuple lives in. + * + * @generated from field: optional string namespace = 1; + */ + namespace?: string + + /** + * The object related by this tuple. + * It is an object in the namespace of the tuple. + * + * @generated from field: optional string object = 2; + */ + object?: string + + /** + * The relation between an Object and a Subject. + * + * @generated from field: optional string relation = 3; + */ + relation?: string + + /** + * The subject related by this tuple. + * A Subject either represents a concrete subject id or + * a `SubjectSet` that expands to more Subjects. + * + * @generated from field: optional ory.keto.relation_tuples.v1alpha2.Subject subject = 4; + */ + subject?: Subject + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationQuery. + * Use `create(RelationQuerySchema)` to create a new message. + */ +export declare const RelationQuerySchema: GenMessage + +/** + * Subject is either a concrete subject id or + * a `SubjectSet` expanding to more Subjects. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.Subject + */ +export declare type Subject = + Message<"ory.keto.relation_tuples.v1alpha2.Subject"> & { + /** + * The reference of this abstract subject. + * + * @generated from oneof ory.keto.relation_tuples.v1alpha2.Subject.ref + */ + ref: + | { + /** + * A concrete id of the subject. + * + * @generated from field: string id = 1; + */ + value: string + case: "id" + } + | { + /** + * A subject set that expands to more Subjects. + * More information are available under [concepts](../concepts/15_subjects.mdx). + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.SubjectSet set = 2; + */ + value: SubjectSet + case: "set" + } + | { case: undefined; value?: undefined } + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.Subject. + * Use `create(SubjectSchema)` to create a new message. + */ +export declare const SubjectSchema: GenMessage + +/** + * SubjectSet refers to all subjects who have + * the same `relation` on an `object`. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.SubjectSet + */ +export declare type SubjectSet = + Message<"ory.keto.relation_tuples.v1alpha2.SubjectSet"> & { + /** + * The namespace of the object and relation + * referenced in this subject set. + * + * @generated from field: string namespace = 1; + */ + namespace: string + + /** + * The object related by this subject set. + * + * @generated from field: string object = 2; + */ + object: string + + /** + * The relation between the object and the subjects. + * + * @generated from field: string relation = 3; + */ + relation: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.SubjectSet. + * Use `create(SubjectSetSchema)` to create a new message. + */ +export declare const SubjectSetSchema: GenMessage diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.js new file mode 100644 index 0000000..9b731c8 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/relation_tuples_pb.js @@ -0,0 +1,49 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/relation_tuples.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { fileDesc, messageDesc } from "@bufbuild/protobuf/codegenv2" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/relation_tuples.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_relation_tuples = + /*@__PURE__*/ + fileDesc( + "Cjdvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvcmVsYXRpb25fdHVwbGVzLnByb3RvEiFvcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIigQEKDVJlbGF0aW9uVHVwbGUSEQoJbmFtZXNwYWNlGAEgASgJEg4KBm9iamVjdBgCIAEoCRIQCghyZWxhdGlvbhgDIAEoCRI7CgdzdWJqZWN0GAQgASgLMioub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlN1YmplY3QixwEKDVJlbGF0aW9uUXVlcnkSFgoJbmFtZXNwYWNlGAEgASgJSACIAQESEwoGb2JqZWN0GAIgASgJSAGIAQESFQoIcmVsYXRpb24YAyABKAlIAogBARJACgdzdWJqZWN0GAQgASgLMioub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLlN1YmplY3RIA4gBAUIMCgpfbmFtZXNwYWNlQgkKB19vYmplY3RCCwoJX3JlbGF0aW9uQgoKCF9zdWJqZWN0IlwKB1N1YmplY3QSDAoCaWQYASABKAlIABI8CgNzZXQYAiABKAsyLS5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuU3ViamVjdFNldEgAQgUKA3JlZiJBCgpTdWJqZWN0U2V0EhEKCW5hbWVzcGFjZRgBIAEoCRIOCgZvYmplY3QYAiABKAkSEAoIcmVsYXRpb24YAyABKAlCrAIKJWNvbS5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTJCE1JlbGF0aW9uVHVwbGVzUHJvdG9QAVpLZ2l0aHViLmNvbS9vcnkva2V0by9nZW4vZ28vb3J5L2tldG8vcmVsYXRpb25fdHVwbGVzL3YxYWxwaGEyO3JlbGF0aW9udHVwbGVzogIDT0tSqgIgT3J5LktldG8uUmVsYXRpb25UdXBsZXMuVjFhbHBoYTLKAiBPcnlcS2V0b1xSZWxhdGlvblR1cGxlc1xWMWFscGhhMuICLE9yeVxLZXRvXFJlbGF0aW9uVHVwbGVzXFYxYWxwaGEyXEdQQk1ldGFkYXRh6gIjT3J5OjpLZXRvOjpSZWxhdGlvblR1cGxlczo6VjFhbHBoYTJiBnByb3RvMw", + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationTuple. + * Use `create(RelationTupleSchema)` to create a new message. + */ +export const RelationTupleSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_relation_tuples, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationQuery. + * Use `create(RelationQuerySchema)` to create a new message. + */ +export const RelationQuerySchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_relation_tuples, 1) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.Subject. + * Use `create(SubjectSchema)` to create a new message. + */ +export const SubjectSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_relation_tuples, 2) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.SubjectSet. + * Use `create(SubjectSetSchema)` to create a new message. + */ +export const SubjectSetSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_relation_tuples, 3) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.d.ts new file mode 100644 index 0000000..6f36cd9 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.d.ts @@ -0,0 +1,34 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/version.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { GetVersionRequest, GetVersionResponse } from "./version_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service returning the specific Ory Keto instance version. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.VersionService + */ +export declare const VersionService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.VersionService" + readonly methods: { + /** + * Returns the version of the Ory Keto instance. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion + */ + readonly getVersion: { + readonly name: "GetVersion" + readonly I: typeof GetVersionRequest + readonly O: typeof GetVersionResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.js new file mode 100644 index 0000000..d33d511 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/version_connect.js @@ -0,0 +1,34 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/version.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { GetVersionRequest, GetVersionResponse } from "./version_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The service returning the specific Ory Keto instance version. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.VersionService + */ +export const VersionService = { + typeName: "ory.keto.relation_tuples.v1alpha2.VersionService", + methods: { + /** + * Returns the version of the Ory Keto instance. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion + */ + getVersion: { + name: "GetVersion", + I: GetVersionRequest, + O: GetVersionResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.d.ts new file mode 100644 index 0000000..461289a --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.d.ts @@ -0,0 +1,73 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/version.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/version.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_version: GenFile + +/** + * Request for the VersionService.GetVersion RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.GetVersionRequest + */ +export declare type GetVersionRequest = + Message<"ory.keto.relation_tuples.v1alpha2.GetVersionRequest"> & {} + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.GetVersionRequest. + * Use `create(GetVersionRequestSchema)` to create a new message. + */ +export declare const GetVersionRequestSchema: GenMessage + +/** + * Response of the VersionService.GetVersion RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.GetVersionResponse + */ +export declare type GetVersionResponse = + Message<"ory.keto.relation_tuples.v1alpha2.GetVersionResponse"> & { + /** + * The version string of the Ory Keto instance. + * + * @generated from field: string version = 1; + */ + version: string + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.GetVersionResponse. + * Use `create(GetVersionResponseSchema)` to create a new message. + */ +export declare const GetVersionResponseSchema: GenMessage + +/** + * The service returning the specific Ory Keto instance version. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.VersionService + */ +export declare const VersionService: GenService<{ + /** + * Returns the version of the Ory Keto instance. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion + */ + getVersion: { + methodKind: "unary" + input: typeof GetVersionRequestSchema + output: typeof GetVersionResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.js new file mode 100644 index 0000000..f11ecbb --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/version_pb.js @@ -0,0 +1,48 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/version.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + fileDesc, + messageDesc, + serviceDesc, +} from "@bufbuild/protobuf/codegenv2" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/version.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_version = + /*@__PURE__*/ + fileDesc( + "Ci9vcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvdmVyc2lvbi5wcm90bxIhb3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyIhMKEUdldFZlcnNpb25SZXF1ZXN0IiUKEkdldFZlcnNpb25SZXNwb25zZRIPCgd2ZXJzaW9uGAEgASgJMosBCg5WZXJzaW9uU2VydmljZRJ5CgpHZXRWZXJzaW9uEjQub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLkdldFZlcnNpb25SZXF1ZXN0GjUub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLkdldFZlcnNpb25SZXNwb25zZUKlAgolY29tLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMkIMVmVyc2lvblByb3RvUAFaS2dpdGh1Yi5jb20vb3J5L2tldG8vZ2VuL2dvL29yeS9rZXRvL3JlbGF0aW9uX3R1cGxlcy92MWFscGhhMjtyZWxhdGlvbnR1cGxlc6ICA09LUqoCIE9yeS5LZXRvLlJlbGF0aW9uVHVwbGVzLlYxYWxwaGEyygIgT3J5XEtldG9cUmVsYXRpb25UdXBsZXNcVjFhbHBoYTLiAixPcnlcS2V0b1xSZWxhdGlvblR1cGxlc1xWMWFscGhhMlxHUEJNZXRhZGF0YeoCI09yeTo6S2V0bzo6UmVsYXRpb25UdXBsZXM6OlYxYWxwaGEyYgZwcm90bzM", + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.GetVersionRequest. + * Use `create(GetVersionRequestSchema)` to create a new message. + */ +export const GetVersionRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_version, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.GetVersionResponse. + * Use `create(GetVersionResponseSchema)` to create a new message. + */ +export const GetVersionResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_version, 1) + +/** + * The service returning the specific Ory Keto instance version. + * + * This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.VersionService + */ +export const VersionService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_version, 0) diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.d.ts new file mode 100644 index 0000000..b1b333b --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.d.ts @@ -0,0 +1,50 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/write_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + DeleteRelationTuplesRequest, + DeleteRelationTuplesResponse, + TransactRelationTuplesRequest, + TransactRelationTuplesResponse, +} from "./write_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The write service to create and delete Access Control Lists. + * + * This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.WriteService + */ +export declare const WriteService: { + readonly typeName: "ory.keto.relation_tuples.v1alpha2.WriteService" + readonly methods: { + /** + * Writes one or more relationships in a single transaction. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples + */ + readonly transactRelationTuples: { + readonly name: "TransactRelationTuples" + readonly I: typeof TransactRelationTuplesRequest + readonly O: typeof TransactRelationTuplesResponse + readonly kind: MethodKind.Unary + } + /** + * Deletes relationships based on relation query + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples + */ + readonly deleteRelationTuples: { + readonly name: "DeleteRelationTuples" + readonly I: typeof DeleteRelationTuplesRequest + readonly O: typeof DeleteRelationTuplesResponse + readonly kind: MethodKind.Unary + } + } +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.js b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.js new file mode 100644 index 0000000..2b4ea14 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_connect.js @@ -0,0 +1,50 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-connect-es v1.7.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/write_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ +// @ts-nocheck + +import { + DeleteRelationTuplesRequest, + DeleteRelationTuplesResponse, + TransactRelationTuplesRequest, + TransactRelationTuplesResponse, +} from "./write_service_pb.js" +import { MethodKind } from "@bufbuild/protobuf" + +/** + * The write service to create and delete Access Control Lists. + * + * This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.WriteService + */ +export const WriteService = { + typeName: "ory.keto.relation_tuples.v1alpha2.WriteService", + methods: { + /** + * Writes one or more relationships in a single transaction. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples + */ + transactRelationTuples: { + name: "TransactRelationTuples", + I: TransactRelationTuplesRequest, + O: TransactRelationTuplesResponse, + kind: MethodKind.Unary, + }, + /** + * Deletes relationships based on relation query + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples + */ + deleteRelationTuples: { + name: "DeleteRelationTuples", + I: DeleteRelationTuplesRequest, + O: DeleteRelationTuplesResponse, + kind: MethodKind.Unary, + }, + }, +} diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.d.ts b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.d.ts new file mode 100644 index 0000000..0ccf349 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.d.ts @@ -0,0 +1,244 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/write_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import type { + GenEnum, + GenFile, + GenMessage, + GenService, +} from "@bufbuild/protobuf/codegenv2" +import type { Message } from "@bufbuild/protobuf" +import type { + RelationQuery, + RelationTuple, + Subject, +} from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/write_service.proto. + */ +export declare const file_ory_keto_relation_tuples_v1alpha2_write_service: GenFile + +/** + * The request of a WriteService.TransactRelationTuples RPC. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest + */ +export declare type TransactRelationTuplesRequest = + Message<"ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest"> & { + /** + * The write delta for the relationships operated in one single transaction. + * Either all actions succeed or no change takes effect on error. + * + * @generated from field: repeated ory.keto.relation_tuples.v1alpha2.RelationTupleDelta relation_tuple_deltas = 1; + */ + relationTupleDeltas: RelationTupleDelta[] + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest. + * Use `create(TransactRelationTuplesRequestSchema)` to create a new message. + */ +export declare const TransactRelationTuplesRequestSchema: GenMessage + +/** + * Write-delta for a TransactRelationTuplesRequest. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.RelationTupleDelta + */ +export declare type RelationTupleDelta = + Message<"ory.keto.relation_tuples.v1alpha2.RelationTupleDelta"> & { + /** + * The action to do on the RelationTuple. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action action = 1; + */ + action: RelationTupleDelta_Action + + /** + * The target RelationTuple. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationTuple relation_tuple = 2; + */ + relationTuple?: RelationTuple + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationTupleDelta. + * Use `create(RelationTupleDeltaSchema)` to create a new message. + */ +export declare const RelationTupleDeltaSchema: GenMessage + +/** + * @generated from enum ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + */ +export enum RelationTupleDelta_Action { + /** + * Unspecified. + * The `TransactRelationTuples` RPC ignores this + * RelationTupleDelta if an action was unspecified. + * + * @generated from enum value: ACTION_UNSPECIFIED = 0; + */ + UNSPECIFIED = 0, + + /** + * Insertion of a new RelationTuple. + * + * @generated from enum value: ACTION_INSERT = 1; + */ + INSERT = 1, + + /** + * Deletion of the RelationTuple. + * It is ignored if it does not exist. + * + * @generated from enum value: ACTION_DELETE = 2; + */ + DELETE = 2, +} + +/** + * Describes the enum ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action. + */ +export declare const RelationTupleDelta_ActionSchema: GenEnum + +/** + * The response of a WriteService.TransactRelationTuples rpc. + * + * @generated from message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse + */ +export declare type TransactRelationTuplesResponse = + Message<"ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse"> & { + /** + * This field is not implemented yet and has no effect. + * + * + * @generated from field: repeated string snaptokens = 1; + */ + snaptokens: string[] + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse. + * Use `create(TransactRelationTuplesResponseSchema)` to create a new message. + */ +export declare const TransactRelationTuplesResponseSchema: GenMessage + +/** + * @generated from message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest + */ +export declare type DeleteRelationTuplesRequest = + Message<"ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest"> & { + /** + * @generated from field: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query query = 1 [deprecated = true]; + * @deprecated + */ + query?: DeleteRelationTuplesRequest_Query + + /** + * @generated from field: ory.keto.relation_tuples.v1alpha2.RelationQuery relation_query = 2; + */ + relationQuery?: RelationQuery + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest. + * Use `create(DeleteRelationTuplesRequestSchema)` to create a new message. + */ +export declare const DeleteRelationTuplesRequestSchema: GenMessage + +/** + * The query for deleting relationships + * + * @generated from message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query + */ +export declare type DeleteRelationTuplesRequest_Query = + Message<"ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query"> & { + /** + * Optional. The namespace to query. + * + * @generated from field: string namespace = 1; + */ + namespace: string + + /** + * Optional. The object to query for. + * + * @generated from field: string object = 2; + */ + object: string + + /** + * Optional. The relation to query for. + * + * @generated from field: string relation = 3; + */ + relation: string + + /** + * Optional. The subject to query for. + * + * @generated from field: ory.keto.relation_tuples.v1alpha2.Subject subject = 4; + */ + subject?: Subject + } + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query. + * Use `create(DeleteRelationTuplesRequest_QuerySchema)` to create a new message. + */ +export declare const DeleteRelationTuplesRequest_QuerySchema: GenMessage + +/** + * @generated from message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse + */ +export declare type DeleteRelationTuplesResponse = + Message<"ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse"> & {} + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse. + * Use `create(DeleteRelationTuplesResponseSchema)` to create a new message. + */ +export declare const DeleteRelationTuplesResponseSchema: GenMessage + +/** + * The write service to create and delete Access Control Lists. + * + * This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.WriteService + */ +export declare const WriteService: GenService<{ + /** + * Writes one or more relationships in a single transaction. + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples + */ + transactRelationTuples: { + methodKind: "unary" + input: typeof TransactRelationTuplesRequestSchema + output: typeof TransactRelationTuplesResponseSchema + } + /** + * Deletes relationships based on relation query + * + * @generated from rpc ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples + */ + deleteRelationTuples: { + methodKind: "unary" + input: typeof DeleteRelationTuplesRequestSchema + output: typeof DeleteRelationTuplesResponseSchema + } +}> diff --git a/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.js b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.js new file mode 100644 index 0000000..37d2d07 --- /dev/null +++ b/gen/es/ory/keto/relation_tuples/v1alpha2/write_service_pb.js @@ -0,0 +1,98 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// @generated by protoc-gen-es v2.11.0 +// @generated from file ory/keto/relation_tuples/v1alpha2/write_service.proto (package ory.keto.relation_tuples.v1alpha2, syntax proto3) +/* eslint-disable */ + +import { + enumDesc, + fileDesc, + messageDesc, + serviceDesc, + tsEnum, +} from "@bufbuild/protobuf/codegenv2" +import { file_ory_keto_relation_tuples_v1alpha2_relation_tuples } from "./relation_tuples_pb" + +/** + * Describes the file ory/keto/relation_tuples/v1alpha2/write_service.proto. + */ +export const file_ory_keto_relation_tuples_v1alpha2_write_service = + /*@__PURE__*/ + fileDesc( + "CjVvcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTIvd3JpdGVfc2VydmljZS5wcm90bxIhb3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyInUKHVRyYW5zYWN0UmVsYXRpb25UdXBsZXNSZXF1ZXN0ElQKFXJlbGF0aW9uX3R1cGxlX2RlbHRhcxgBIAMoCzI1Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5SZWxhdGlvblR1cGxlRGVsdGEi9AEKElJlbGF0aW9uVHVwbGVEZWx0YRJMCgZhY3Rpb24YASABKA4yPC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuUmVsYXRpb25UdXBsZURlbHRhLkFjdGlvbhJICg5yZWxhdGlvbl90dXBsZRgCIAEoCzIwLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5SZWxhdGlvblR1cGxlIkYKBkFjdGlvbhIWChJBQ1RJT05fVU5TUEVDSUZJRUQQABIRCg1BQ1RJT05fSU5TRVJUEAESEQoNQUNUSU9OX0RFTEVURRACIjQKHlRyYW5zYWN0UmVsYXRpb25UdXBsZXNSZXNwb25zZRISCgpzbmFwdG9rZW5zGAEgAygJIrsCChtEZWxldGVSZWxhdGlvblR1cGxlc1JlcXVlc3QSVwoFcXVlcnkYASABKAsyRC5vcnkua2V0by5yZWxhdGlvbl90dXBsZXMudjFhbHBoYTIuRGVsZXRlUmVsYXRpb25UdXBsZXNSZXF1ZXN0LlF1ZXJ5QgIYARJICg5yZWxhdGlvbl9xdWVyeRgCIAEoCzIwLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5SZWxhdGlvblF1ZXJ5GnkKBVF1ZXJ5EhEKCW5hbWVzcGFjZRgBIAEoCRIOCgZvYmplY3QYAiABKAkSEAoIcmVsYXRpb24YAyABKAkSOwoHc3ViamVjdBgEIAEoCzIqLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5TdWJqZWN0Ih4KHERlbGV0ZVJlbGF0aW9uVHVwbGVzUmVzcG9uc2UyyAIKDFdyaXRlU2VydmljZRKdAQoWVHJhbnNhY3RSZWxhdGlvblR1cGxlcxJALm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5UcmFuc2FjdFJlbGF0aW9uVHVwbGVzUmVxdWVzdBpBLm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5UcmFuc2FjdFJlbGF0aW9uVHVwbGVzUmVzcG9uc2USlwEKFERlbGV0ZVJlbGF0aW9uVHVwbGVzEj4ub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyLkRlbGV0ZVJlbGF0aW9uVHVwbGVzUmVxdWVzdBo/Lm9yeS5rZXRvLnJlbGF0aW9uX3R1cGxlcy52MWFscGhhMi5EZWxldGVSZWxhdGlvblR1cGxlc1Jlc3BvbnNlQqoCCiVjb20ub3J5LmtldG8ucmVsYXRpb25fdHVwbGVzLnYxYWxwaGEyQhFXcml0ZVNlcnZpY2VQcm90b1ABWktnaXRodWIuY29tL29yeS9rZXRvL2dlbi9nby9vcnkva2V0by9yZWxhdGlvbl90dXBsZXMvdjFhbHBoYTI7cmVsYXRpb250dXBsZXOiAgNPS1KqAiBPcnkuS2V0by5SZWxhdGlvblR1cGxlcy5WMWFscGhhMsoCIE9yeVxLZXRvXFJlbGF0aW9uVHVwbGVzXFYxYWxwaGEy4gIsT3J5XEtldG9cUmVsYXRpb25UdXBsZXNcVjFhbHBoYTJcR1BCTWV0YWRhdGHqAiNPcnk6OktldG86OlJlbGF0aW9uVHVwbGVzOjpWMWFscGhhMmIGcHJvdG8z", + [file_ory_keto_relation_tuples_v1alpha2_relation_tuples], + ) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest. + * Use `create(TransactRelationTuplesRequestSchema)` to create a new message. + */ +export const TransactRelationTuplesRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.RelationTupleDelta. + * Use `create(RelationTupleDeltaSchema)` to create a new message. + */ +export const RelationTupleDeltaSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 1) + +/** + * Describes the enum ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action. + */ +export const RelationTupleDelta_ActionSchema = + /*@__PURE__*/ + enumDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 1, 0) + +/** + * @generated from enum ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + */ +export const RelationTupleDelta_Action = + /*@__PURE__*/ + tsEnum(RelationTupleDelta_ActionSchema) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse. + * Use `create(TransactRelationTuplesResponseSchema)` to create a new message. + */ +export const TransactRelationTuplesResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 2) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest. + * Use `create(DeleteRelationTuplesRequestSchema)` to create a new message. + */ +export const DeleteRelationTuplesRequestSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 3) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query. + * Use `create(DeleteRelationTuplesRequest_QuerySchema)` to create a new message. + */ +export const DeleteRelationTuplesRequest_QuerySchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 3, 0) + +/** + * Describes the message ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse. + * Use `create(DeleteRelationTuplesResponseSchema)` to create a new message. + */ +export const DeleteRelationTuplesResponseSchema = + /*@__PURE__*/ + messageDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 4) + +/** + * The write service to create and delete Access Control Lists. + * + * This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). + * + * @generated from service ory.keto.relation_tuples.v1alpha2.WriteService + */ +export const WriteService = + /*@__PURE__*/ + serviceDesc(file_ory_keto_relation_tuples_v1alpha2_write_service, 0) diff --git a/gen/es/package-lock.json b/gen/es/package-lock.json new file mode 100644 index 0000000..461571a --- /dev/null +++ b/gen/es/package-lock.json @@ -0,0 +1,28 @@ +{ + "name": "@ory/keto-grpc-client", + "version": "0.6.0-alpha.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@ory/keto-grpc-client", + "version": "0.6.0-alpha.1", + "dependencies": { + "@bufbuild/protobuf": "^2.11.0" + } + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + } + }, + "dependencies": { + "@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==" + } + } +} diff --git a/gen/es/package.json b/gen/es/package.json new file mode 100644 index 0000000..7d4e351 --- /dev/null +++ b/gen/es/package.json @@ -0,0 +1,10 @@ +{ + "name": "@ory/keto-grpc-client", + "version": "0.6.0-alpha.1", + "description": "gRPC client for Ory Keto", + "homepage": "https://www.ory.com/keto", + "bugs": "https://github.com/ory/keto/issues", + "dependencies": { + "@bufbuild/protobuf": "^2.11.0" + } +} diff --git a/gen/go/LICENSE b/gen/go/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/gen/go/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + 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 "[]" + 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 + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + 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/gen/go/go.mod b/gen/go/go.mod new file mode 100644 index 0000000..b8c51e9 --- /dev/null +++ b/gen/go/go.mod @@ -0,0 +1,23 @@ +module github.com/ory/keto/gen/go + +go 1.25.5 + +require ( + connectrpc.com/connect v1.20.0 + github.com/stretchr/testify v1.11.1 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/gen/go/go.sum b/gen/go/go.sum new file mode 100644 index 0000000..b0f65f5 --- /dev/null +++ b/gen/go/go.sum @@ -0,0 +1,63 @@ +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gen/go/ory/keto/opl/v1alpha1/oplconnect/syntax_service.connect.go b/gen/go/ory/keto/opl/v1alpha1/oplconnect/syntax_service.connect.go new file mode 100644 index 0000000..1ba2e32 --- /dev/null +++ b/gen/go/ory/keto/opl/v1alpha1/oplconnect/syntax_service.connect.go @@ -0,0 +1,113 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/opl/v1alpha1/syntax_service.proto + +package oplconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha1 "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // SyntaxServiceName is the fully-qualified name of the SyntaxService service. + SyntaxServiceName = "ory.keto.opl.v1alpha1.SyntaxService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // SyntaxServiceCheckProcedure is the fully-qualified name of the SyntaxService's Check RPC. + SyntaxServiceCheckProcedure = "/ory.keto.opl.v1alpha1.SyntaxService/Check" +) + +// SyntaxServiceClient is a client for the ory.keto.opl.v1alpha1.SyntaxService service. +type SyntaxServiceClient interface { + // Performs a syntax check request. + Check(context.Context, *connect.Request[v1alpha1.CheckRequest]) (*connect.Response[v1alpha1.CheckResponse], error) +} + +// NewSyntaxServiceClient constructs a client for the ory.keto.opl.v1alpha1.SyntaxService service. +// By default, it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped +// responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the +// connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewSyntaxServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) SyntaxServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + syntaxServiceMethods := v1alpha1.File_ory_keto_opl_v1alpha1_syntax_service_proto.Services().ByName("SyntaxService").Methods() + return &syntaxServiceClient{ + check: connect.NewClient[v1alpha1.CheckRequest, v1alpha1.CheckResponse]( + httpClient, + baseURL+SyntaxServiceCheckProcedure, + connect.WithSchema(syntaxServiceMethods.ByName("Check")), + connect.WithClientOptions(opts...), + ), + } +} + +// syntaxServiceClient implements SyntaxServiceClient. +type syntaxServiceClient struct { + check *connect.Client[v1alpha1.CheckRequest, v1alpha1.CheckResponse] +} + +// Check calls ory.keto.opl.v1alpha1.SyntaxService.Check. +func (c *syntaxServiceClient) Check(ctx context.Context, req *connect.Request[v1alpha1.CheckRequest]) (*connect.Response[v1alpha1.CheckResponse], error) { + return c.check.CallUnary(ctx, req) +} + +// SyntaxServiceHandler is an implementation of the ory.keto.opl.v1alpha1.SyntaxService service. +type SyntaxServiceHandler interface { + // Performs a syntax check request. + Check(context.Context, *connect.Request[v1alpha1.CheckRequest]) (*connect.Response[v1alpha1.CheckResponse], error) +} + +// NewSyntaxServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewSyntaxServiceHandler(svc SyntaxServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + syntaxServiceMethods := v1alpha1.File_ory_keto_opl_v1alpha1_syntax_service_proto.Services().ByName("SyntaxService").Methods() + syntaxServiceCheckHandler := connect.NewUnaryHandler( + SyntaxServiceCheckProcedure, + svc.Check, + connect.WithSchema(syntaxServiceMethods.ByName("Check")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.opl.v1alpha1.SyntaxService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case SyntaxServiceCheckProcedure: + syntaxServiceCheckHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedSyntaxServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedSyntaxServiceHandler struct{} + +func (UnimplementedSyntaxServiceHandler) Check(context.Context, *connect.Request[v1alpha1.CheckRequest]) (*connect.Response[v1alpha1.CheckResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.opl.v1alpha1.SyntaxService.Check is not implemented")) +} diff --git a/gen/go/ory/keto/opl/v1alpha1/syntax_service.pb.go b/gen/go/ory/keto/opl/v1alpha1/syntax_service.pb.go new file mode 100644 index 0000000..ffcc3a4 --- /dev/null +++ b/gen/go/ory/keto/opl/v1alpha1/syntax_service.pb.go @@ -0,0 +1,302 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/opl/v1alpha1/syntax_service.proto + +package opl + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckRequest) Reset() { + *x = CheckRequest{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckRequest) ProtoMessage() {} + +func (x *CheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead. +func (*CheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{0} +} + +func (x *CheckRequest) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +type CheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ParseErrors []*ParseError `protobuf:"bytes,1,rep,name=parse_errors,json=parseErrors,proto3" json:"parse_errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponse) Reset() { + *x = CheckResponse{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponse) ProtoMessage() {} + +func (x *CheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponse.ProtoReflect.Descriptor instead. +func (*CheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckResponse) GetParseErrors() []*ParseError { + if x != nil { + return x.ParseErrors + } + return nil +} + +type ParseError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Start *SourcePosition `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"` + End *SourcePosition `protobuf:"bytes,3,opt,name=end,proto3" json:"end,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParseError) Reset() { + *x = ParseError{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParseError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseError) ProtoMessage() {} + +func (x *ParseError) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseError.ProtoReflect.Descriptor instead. +func (*ParseError) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ParseError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ParseError) GetStart() *SourcePosition { + if x != nil { + return x.Start + } + return nil +} + +func (x *ParseError) GetEnd() *SourcePosition { + if x != nil { + return x.End + } + return nil +} + +type SourcePosition struct { + state protoimpl.MessageState `protogen:"open.v1"` + Line uint32 `protobuf:"varint,1,opt,name=line,proto3" json:"line,omitempty"` + Column uint32 `protobuf:"varint,2,opt,name=column,proto3" json:"column,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourcePosition) Reset() { + *x = SourcePosition{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourcePosition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourcePosition) ProtoMessage() {} + +func (x *SourcePosition) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourcePosition.ProtoReflect.Descriptor instead. +func (*SourcePosition) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{3} +} + +func (x *SourcePosition) GetLine() uint32 { + if x != nil { + return x.Line + } + return 0 +} + +func (x *SourcePosition) GetColumn() uint32 { + if x != nil { + return x.Column + } + return 0 +} + +var File_ory_keto_opl_v1alpha1_syntax_service_proto protoreflect.FileDescriptor + +const file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc = "" + + "\n" + + "*ory/keto/opl/v1alpha1/syntax_service.proto\x12\x15ory.keto.opl.v1alpha1\"(\n" + + "\fCheckRequest\x12\x18\n" + + "\acontent\x18\x01 \x01(\fR\acontent\"U\n" + + "\rCheckResponse\x12D\n" + + "\fparse_errors\x18\x01 \x03(\v2!.ory.keto.opl.v1alpha1.ParseErrorR\vparseErrors\"\x9c\x01\n" + + "\n" + + "ParseError\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12;\n" + + "\x05start\x18\x02 \x01(\v2%.ory.keto.opl.v1alpha1.SourcePositionR\x05start\x127\n" + + "\x03end\x18\x03 \x01(\v2%.ory.keto.opl.v1alpha1.SourcePositionR\x03end\"<\n" + + "\x0eSourcePosition\x12\x12\n" + + "\x04line\x18\x01 \x01(\rR\x04line\x12\x16\n" + + "\x06column\x18\x02 \x01(\rR\x06column2c\n" + + "\rSyntaxService\x12R\n" + + "\x05Check\x12#.ory.keto.opl.v1alpha1.CheckRequest\x1a$.ory.keto.opl.v1alpha1.CheckResponseB\xdc\x01\n" + + "\x19com.ory.keto.opl.v1alpha1B\x12SyntaxServiceProtoP\x01Z4github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1;opl\xa2\x02\x03OKO\xaa\x02\x15Ory.Keto.Opl.V1alpha1\xca\x02\x15Ory\\Keto\\Opl\\V1alpha1\xe2\x02!Ory\\Keto\\Opl\\V1alpha1\\GPBMetadata\xea\x02\x18Ory::Keto::Opl::V1alpha1b\x06proto3" + +var ( + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescOnce sync.Once + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData []byte +) + +func file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP() []byte { + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescOnce.Do(func() { + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc), len(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc))) + }) + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData +} + +var file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes = []any{ + (*CheckRequest)(nil), // 0: ory.keto.opl.v1alpha1.CheckRequest + (*CheckResponse)(nil), // 1: ory.keto.opl.v1alpha1.CheckResponse + (*ParseError)(nil), // 2: ory.keto.opl.v1alpha1.ParseError + (*SourcePosition)(nil), // 3: ory.keto.opl.v1alpha1.SourcePosition +} +var file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.opl.v1alpha1.CheckResponse.parse_errors:type_name -> ory.keto.opl.v1alpha1.ParseError + 3, // 1: ory.keto.opl.v1alpha1.ParseError.start:type_name -> ory.keto.opl.v1alpha1.SourcePosition + 3, // 2: ory.keto.opl.v1alpha1.ParseError.end:type_name -> ory.keto.opl.v1alpha1.SourcePosition + 0, // 3: ory.keto.opl.v1alpha1.SyntaxService.Check:input_type -> ory.keto.opl.v1alpha1.CheckRequest + 1, // 4: ory.keto.opl.v1alpha1.SyntaxService.Check:output_type -> ory.keto.opl.v1alpha1.CheckResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ory_keto_opl_v1alpha1_syntax_service_proto_init() } +func file_ory_keto_opl_v1alpha1_syntax_service_proto_init() { + if File_ory_keto_opl_v1alpha1_syntax_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc), len(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes, + DependencyIndexes: file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs, + MessageInfos: file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes, + }.Build() + File_ory_keto_opl_v1alpha1_syntax_service_proto = out.File + file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes = nil + file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go b/gen/go/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go new file mode 100644 index 0000000..f1302d7 --- /dev/null +++ b/gen/go/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go @@ -0,0 +1,130 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/opl/v1alpha1/syntax_service.proto + +package opl + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SyntaxService_Check_FullMethodName = "/ory.keto.opl.v1alpha1.SyntaxService/Check" +) + +// SyntaxServiceClient is the client API for SyntaxService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that checks the syntax of an OPL file. +type SyntaxServiceClient interface { + // Performs a syntax check request. + Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) +} + +type syntaxServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSyntaxServiceClient(cc grpc.ClientConnInterface) SyntaxServiceClient { + return &syntaxServiceClient{cc} +} + +func (c *syntaxServiceClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckResponse) + err := c.cc.Invoke(ctx, SyntaxService_Check_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SyntaxServiceServer is the server API for SyntaxService service. +// All implementations must embed UnimplementedSyntaxServiceServer +// for forward compatibility. +// +// The service that checks the syntax of an OPL file. +type SyntaxServiceServer interface { + // Performs a syntax check request. + Check(context.Context, *CheckRequest) (*CheckResponse, error) + mustEmbedUnimplementedSyntaxServiceServer() +} + +// UnimplementedSyntaxServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSyntaxServiceServer struct{} + +func (UnimplementedSyntaxServiceServer) Check(context.Context, *CheckRequest) (*CheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedSyntaxServiceServer) mustEmbedUnimplementedSyntaxServiceServer() {} +func (UnimplementedSyntaxServiceServer) testEmbeddedByValue() {} + +// UnsafeSyntaxServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SyntaxServiceServer will +// result in compilation errors. +type UnsafeSyntaxServiceServer interface { + mustEmbedUnimplementedSyntaxServiceServer() +} + +func RegisterSyntaxServiceServer(s grpc.ServiceRegistrar, srv SyntaxServiceServer) { + // If the following call panics, it indicates UnimplementedSyntaxServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SyntaxService_ServiceDesc, srv) +} + +func _SyntaxService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyntaxServiceServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyntaxService_Check_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyntaxServiceServer).Check(ctx, req.(*CheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SyntaxService_ServiceDesc is the grpc.ServiceDesc for SyntaxService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SyntaxService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.opl.v1alpha1.SyntaxService", + HandlerType: (*SyntaxServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _SyntaxService_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/opl/v1alpha1/syntax_service.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/check_service.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/check_service.pb.go new file mode 100644 index 0000000..7e8b8bd --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/check_service.pb.go @@ -0,0 +1,594 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/check_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request for a CheckService.Check RPC. +// Checks whether a specific subject is related to an object. +type CheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace to evaluate the check. + // + // Note: If you use the expand-API and the check + // evaluates a RelationTuple specifying a SubjectSet as + // subject or due to a rewrite rule in a namespace config + // this check request may involve other namespaces automatically. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The related object in this check. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between the Object and the Subject. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // The related subject in this check. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + Tuple *RelationTuple `protobuf:"bytes,8,opt,name=tuple,proto3" json:"tuple,omitempty"` + // This field is not implemented yet and has no effect. + // + Latest bool `protobuf:"varint,5,opt,name=latest,proto3" json:"latest,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,6,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + MaxDepth int32 `protobuf:"varint,7,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckRequest) Reset() { + *x = CheckRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckRequest) ProtoMessage() {} + +func (x *CheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead. +func (*CheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *CheckRequest) GetTuple() *RelationTuple { + if x != nil { + return x.Tuple + } + return nil +} + +func (x *CheckRequest) GetLatest() bool { + if x != nil { + return x.Latest + } + return false +} + +func (x *CheckRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *CheckRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +// The response for a CheckService.Check rpc. +type CheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,2,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponse) Reset() { + *x = CheckResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponse) ProtoMessage() {} + +func (x *CheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponse.ProtoReflect.Descriptor instead. +func (*CheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckResponse) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *CheckResponse) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The response for an individual check in the CheckService.BatchCheck rpc. +type CheckResponseWithError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + // If there was an error checking the tuple, + // this will contain the error message. + // + // If the check was performed successfully, this will be empty. + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponseWithError) Reset() { + *x = CheckResponseWithError{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponseWithError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponseWithError) ProtoMessage() {} + +func (x *CheckResponseWithError) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponseWithError.ProtoReflect.Descriptor instead. +func (*CheckResponseWithError) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckResponseWithError) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *CheckResponseWithError) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *CheckResponseWithError) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The request for a CheckService.BatchCheck RPC. +// Checks a batch of relations. +type BatchCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tuples []*RelationTuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"` + // This field is not implemented yet and has no effect. + // + Latest bool `protobuf:"varint,2,opt,name=latest,proto3" json:"latest,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + MaxDepth int32 `protobuf:"varint,4,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchCheckRequest) Reset() { + *x = BatchCheckRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchCheckRequest) ProtoMessage() {} + +func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. +func (*BatchCheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{3} +} + +func (x *BatchCheckRequest) GetTuples() []*RelationTuple { + if x != nil { + return x.Tuples + } + return nil +} + +func (x *BatchCheckRequest) GetLatest() bool { + if x != nil { + return x.Latest + } + return false +} + +func (x *BatchCheckRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *BatchCheckRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +// The response for a CheckService.BatchCheck rpc. +type BatchCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The results of the batch check. The order of these + // results will match the order of the input. + Results []*CheckResponseWithError `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchCheckResponse) Reset() { + *x = BatchCheckResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchCheckResponse) ProtoMessage() {} + +func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. +func (*BatchCheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{4} +} + +func (x *BatchCheckResponse) GetResults() []*CheckResponseWithError { + if x != nil { + return x.Results + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_check_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc = "" + + "\n" + + "5ory/keto/relation_tuples/v1alpha2/check_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\xd1\x02\n" + + "\fCheckRequest\x12 \n" + + "\tnamespace\x18\x01 \x01(\tB\x02\x18\x01R\tnamespace\x12\x1a\n" + + "\x06object\x18\x02 \x01(\tB\x02\x18\x01R\x06object\x12\x1e\n" + + "\brelation\x18\x03 \x01(\tB\x02\x18\x01R\brelation\x12H\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectB\x02\x18\x01R\asubject\x12F\n" + + "\x05tuple\x18\b \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x05tuple\x12\x16\n" + + "\x06latest\x18\x05 \x01(\bR\x06latest\x12\x1c\n" + + "\tsnaptoken\x18\x06 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tmax_depth\x18\a \x01(\x05R\bmaxDepth\"G\n" + + "\rCheckResponse\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12\x1c\n" + + "\tsnaptoken\x18\x02 \x01(\tR\tsnaptoken\"f\n" + + "\x16CheckResponseWithError\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\"\xb0\x01\n" + + "\x11BatchCheckRequest\x12H\n" + + "\x06tuples\x18\x01 \x03(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x06tuples\x12\x16\n" + + "\x06latest\x18\x02 \x01(\bR\x06latest\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tmax_depth\x18\x04 \x01(\x05R\bmaxDepth\"i\n" + + "\x12BatchCheckResponse\x12S\n" + + "\aresults\x18\x01 \x03(\v29.ory.keto.relation_tuples.v1alpha2.CheckResponseWithErrorR\aresults2\xf5\x01\n" + + "\fCheckService\x12j\n" + + "\x05Check\x12/.ory.keto.relation_tuples.v1alpha2.CheckRequest\x1a0.ory.keto.relation_tuples.v1alpha2.CheckResponse\x12y\n" + + "\n" + + "BatchCheck\x124.ory.keto.relation_tuples.v1alpha2.BatchCheckRequest\x1a5.ory.keto.relation_tuples.v1alpha2.BatchCheckResponseB\xaa\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x11CheckServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes = []any{ + (*CheckRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.CheckRequest + (*CheckResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.CheckResponse + (*CheckResponseWithError)(nil), // 2: ory.keto.relation_tuples.v1alpha2.CheckResponseWithError + (*BatchCheckRequest)(nil), // 3: ory.keto.relation_tuples.v1alpha2.BatchCheckRequest + (*BatchCheckResponse)(nil), // 4: ory.keto.relation_tuples.v1alpha2.BatchCheckResponse + (*Subject)(nil), // 5: ory.keto.relation_tuples.v1alpha2.Subject + (*RelationTuple)(nil), // 6: ory.keto.relation_tuples.v1alpha2.RelationTuple +} +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs = []int32{ + 5, // 0: ory.keto.relation_tuples.v1alpha2.CheckRequest.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 6, // 1: ory.keto.relation_tuples.v1alpha2.CheckRequest.tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 2: ory.keto.relation_tuples.v1alpha2.BatchCheckRequest.tuples:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 2, // 3: ory.keto.relation_tuples.v1alpha2.BatchCheckResponse.results:type_name -> ory.keto.relation_tuples.v1alpha2.CheckResponseWithError + 0, // 4: ory.keto.relation_tuples.v1alpha2.CheckService.Check:input_type -> ory.keto.relation_tuples.v1alpha2.CheckRequest + 3, // 5: ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck:input_type -> ory.keto.relation_tuples.v1alpha2.BatchCheckRequest + 1, // 6: ory.keto.relation_tuples.v1alpha2.CheckService.Check:output_type -> ory.keto.relation_tuples.v1alpha2.CheckResponse + 4, // 7: ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck:output_type -> ory.keto.relation_tuples.v1alpha2.BatchCheckResponse + 6, // [6:8] is the sub-list for method output_type + 4, // [4:6] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_check_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_check_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_check_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_check_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go new file mode 100644 index 0000000..95e856c --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go @@ -0,0 +1,174 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/check_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + CheckService_Check_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.CheckService/Check" + CheckService_BatchCheck_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.CheckService/BatchCheck" +) + +// CheckServiceClient is the client API for CheckService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that performs authorization checks +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type CheckServiceClient interface { + // Performs an authorization check. + Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) + BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) +} + +type checkServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCheckServiceClient(cc grpc.ClientConnInterface) CheckServiceClient { + return &checkServiceClient{cc} +} + +func (c *checkServiceClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckResponse) + err := c.cc.Invoke(ctx, CheckService_Check_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *checkServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BatchCheckResponse) + err := c.cc.Invoke(ctx, CheckService_BatchCheck_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CheckServiceServer is the server API for CheckService service. +// All implementations must embed UnimplementedCheckServiceServer +// for forward compatibility. +// +// The service that performs authorization checks +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type CheckServiceServer interface { + // Performs an authorization check. + Check(context.Context, *CheckRequest) (*CheckResponse, error) + BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) + mustEmbedUnimplementedCheckServiceServer() +} + +// UnimplementedCheckServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedCheckServiceServer struct{} + +func (UnimplementedCheckServiceServer) Check(context.Context, *CheckRequest) (*CheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedCheckServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchCheck not implemented") +} +func (UnimplementedCheckServiceServer) mustEmbedUnimplementedCheckServiceServer() {} +func (UnimplementedCheckServiceServer) testEmbeddedByValue() {} + +// UnsafeCheckServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CheckServiceServer will +// result in compilation errors. +type UnsafeCheckServiceServer interface { + mustEmbedUnimplementedCheckServiceServer() +} + +func RegisterCheckServiceServer(s grpc.ServiceRegistrar, srv CheckServiceServer) { + // If the following call panics, it indicates UnimplementedCheckServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&CheckService_ServiceDesc, srv) +} + +func _CheckService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CheckServiceServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CheckService_Check_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CheckServiceServer).Check(ctx, req.(*CheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CheckService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CheckServiceServer).BatchCheck(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CheckService_BatchCheck_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CheckServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CheckService_ServiceDesc is the grpc.ServiceDesc for CheckService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CheckService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.CheckService", + HandlerType: (*CheckServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _CheckService_Check_Handler, + }, + { + MethodName: "BatchCheck", + Handler: _CheckService_BatchCheck_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/check_service.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go new file mode 100644 index 0000000..a971502 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go @@ -0,0 +1,388 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/expand_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NodeType int32 + +const ( + NodeType_NODE_TYPE_UNSPECIFIED NodeType = 0 + // This node expands to a union of all children. + NodeType_NODE_TYPE_UNION NodeType = 1 + // Not implemented yet. + NodeType_NODE_TYPE_EXCLUSION NodeType = 2 + // Not implemented yet. + NodeType_NODE_TYPE_INTERSECTION NodeType = 3 + // This node is a leaf and contains no children. + // Its subject is a `SubjectID` unless `max_depth` was reached. + NodeType_NODE_TYPE_LEAF NodeType = 4 +) + +// Enum value maps for NodeType. +var ( + NodeType_name = map[int32]string{ + 0: "NODE_TYPE_UNSPECIFIED", + 1: "NODE_TYPE_UNION", + 2: "NODE_TYPE_EXCLUSION", + 3: "NODE_TYPE_INTERSECTION", + 4: "NODE_TYPE_LEAF", + } + NodeType_value = map[string]int32{ + "NODE_TYPE_UNSPECIFIED": 0, + "NODE_TYPE_UNION": 1, + "NODE_TYPE_EXCLUSION": 2, + "NODE_TYPE_INTERSECTION": 3, + "NODE_TYPE_LEAF": 4, + } +) + +func (x NodeType) Enum() *NodeType { + p := new(NodeType) + *p = x + return p +} + +func (x NodeType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NodeType) Descriptor() protoreflect.EnumDescriptor { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes[0].Descriptor() +} + +func (NodeType) Type() protoreflect.EnumType { + return &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes[0] +} + +func (x NodeType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NodeType.Descriptor instead. +func (NodeType) EnumDescriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{0} +} + +// The request for an ExpandService.Expand RPC. +// Expands the given subject set. +type ExpandRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The subject to expand. + Subject *Subject `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + // The maximum depth of tree to build. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + // + // It is important to set this parameter to a meaningful + // value. Ponder how deep you really want to display this. + MaxDepth int32 `protobuf:"varint,2,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpandRequest) Reset() { + *x = ExpandRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpandRequest) ProtoMessage() {} + +func (x *ExpandRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpandRequest.ProtoReflect.Descriptor instead. +func (*ExpandRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ExpandRequest) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *ExpandRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +func (x *ExpandRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The response for a ExpandService.Expand RPC. +type ExpandResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The tree the requested subject set expands to. + // The requested subject set is the subject of the root. + // + // This field can be nil in some circumstances. + Tree *SubjectTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpandResponse) Reset() { + *x = ExpandResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpandResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpandResponse) ProtoMessage() {} + +func (x *ExpandResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpandResponse.ProtoReflect.Descriptor instead. +func (*ExpandResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ExpandResponse) GetTree() *SubjectTree { + if x != nil { + return x.Tree + } + return nil +} + +type SubjectTree struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of the node. + NodeType NodeType `protobuf:"varint,1,opt,name=node_type,json=nodeType,proto3,enum=ory.keto.relation_tuples.v1alpha2.NodeType" json:"node_type,omitempty"` + // The subject this node represents. + // Deprecated: More information is now available in the tuple field. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/expand_service.proto. + Subject *Subject `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` + // The relation tuple this node represents. + Tuple *RelationTuple `protobuf:"bytes,4,opt,name=tuple,proto3" json:"tuple,omitempty"` + // The children of this node. + // + // This is never set if `node_type` == `NODE_TYPE_LEAF`. + Children []*SubjectTree `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubjectTree) Reset() { + *x = SubjectTree{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubjectTree) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectTree) ProtoMessage() {} + +func (x *SubjectTree) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectTree.ProtoReflect.Descriptor instead. +func (*SubjectTree) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{2} +} + +func (x *SubjectTree) GetNodeType() NodeType { + if x != nil { + return x.NodeType + } + return NodeType_NODE_TYPE_UNSPECIFIED +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/expand_service.proto. +func (x *SubjectTree) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *SubjectTree) GetTuple() *RelationTuple { + if x != nil { + return x.Tuple + } + return nil +} + +func (x *SubjectTree) GetChildren() []*SubjectTree { + if x != nil { + return x.Children + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_expand_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc = "" + + "\n" + + "6ory/keto/relation_tuples/v1alpha2/expand_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\x90\x01\n" + + "\rExpandRequest\x12D\n" + + "\asubject\x18\x01 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\x12\x1b\n" + + "\tmax_depth\x18\x02 \x01(\x05R\bmaxDepth\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\"T\n" + + "\x0eExpandResponse\x12B\n" + + "\x04tree\x18\x01 \x01(\v2..ory.keto.relation_tuples.v1alpha2.SubjectTreeR\x04tree\"\xb5\x02\n" + + "\vSubjectTree\x12H\n" + + "\tnode_type\x18\x01 \x01(\x0e2+.ory.keto.relation_tuples.v1alpha2.NodeTypeR\bnodeType\x12H\n" + + "\asubject\x18\x02 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectB\x02\x18\x01R\asubject\x12F\n" + + "\x05tuple\x18\x04 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x05tuple\x12J\n" + + "\bchildren\x18\x03 \x03(\v2..ory.keto.relation_tuples.v1alpha2.SubjectTreeR\bchildren*\x83\x01\n" + + "\bNodeType\x12\x19\n" + + "\x15NODE_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fNODE_TYPE_UNION\x10\x01\x12\x17\n" + + "\x13NODE_TYPE_EXCLUSION\x10\x02\x12\x1a\n" + + "\x16NODE_TYPE_INTERSECTION\x10\x03\x12\x12\n" + + "\x0eNODE_TYPE_LEAF\x10\x042~\n" + + "\rExpandService\x12m\n" + + "\x06Expand\x120.ory.keto.relation_tuples.v1alpha2.ExpandRequest\x1a1.ory.keto.relation_tuples.v1alpha2.ExpandResponseB\xab\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x12ExpandServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes = []any{ + (NodeType)(0), // 0: ory.keto.relation_tuples.v1alpha2.NodeType + (*ExpandRequest)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ExpandRequest + (*ExpandResponse)(nil), // 2: ory.keto.relation_tuples.v1alpha2.ExpandResponse + (*SubjectTree)(nil), // 3: ory.keto.relation_tuples.v1alpha2.SubjectTree + (*Subject)(nil), // 4: ory.keto.relation_tuples.v1alpha2.Subject + (*RelationTuple)(nil), // 5: ory.keto.relation_tuples.v1alpha2.RelationTuple +} +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs = []int32{ + 4, // 0: ory.keto.relation_tuples.v1alpha2.ExpandRequest.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 3, // 1: ory.keto.relation_tuples.v1alpha2.ExpandResponse.tree:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectTree + 0, // 2: ory.keto.relation_tuples.v1alpha2.SubjectTree.node_type:type_name -> ory.keto.relation_tuples.v1alpha2.NodeType + 4, // 3: ory.keto.relation_tuples.v1alpha2.SubjectTree.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 5, // 4: ory.keto.relation_tuples.v1alpha2.SubjectTree.tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 3, // 5: ory.keto.relation_tuples.v1alpha2.SubjectTree.children:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectTree + 1, // 6: ory.keto.relation_tuples.v1alpha2.ExpandService.Expand:input_type -> ory.keto.relation_tuples.v1alpha2.ExpandRequest + 2, // 7: ory.keto.relation_tuples.v1alpha2.ExpandService.Expand:output_type -> ory.keto.relation_tuples.v1alpha2.ExpandResponse + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_expand_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs, + EnumInfos: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_expand_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go new file mode 100644 index 0000000..d1d422a --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go @@ -0,0 +1,136 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/expand_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ExpandService_Expand_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.ExpandService/Expand" +) + +// ExpandServiceClient is the client API for ExpandService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that performs subject set expansion +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ExpandServiceClient interface { + // Expands the subject set into a tree of subjects. + Expand(ctx context.Context, in *ExpandRequest, opts ...grpc.CallOption) (*ExpandResponse, error) +} + +type expandServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewExpandServiceClient(cc grpc.ClientConnInterface) ExpandServiceClient { + return &expandServiceClient{cc} +} + +func (c *expandServiceClient) Expand(ctx context.Context, in *ExpandRequest, opts ...grpc.CallOption) (*ExpandResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExpandResponse) + err := c.cc.Invoke(ctx, ExpandService_Expand_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ExpandServiceServer is the server API for ExpandService service. +// All implementations must embed UnimplementedExpandServiceServer +// for forward compatibility. +// +// The service that performs subject set expansion +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ExpandServiceServer interface { + // Expands the subject set into a tree of subjects. + Expand(context.Context, *ExpandRequest) (*ExpandResponse, error) + mustEmbedUnimplementedExpandServiceServer() +} + +// UnimplementedExpandServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedExpandServiceServer struct{} + +func (UnimplementedExpandServiceServer) Expand(context.Context, *ExpandRequest) (*ExpandResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Expand not implemented") +} +func (UnimplementedExpandServiceServer) mustEmbedUnimplementedExpandServiceServer() {} +func (UnimplementedExpandServiceServer) testEmbeddedByValue() {} + +// UnsafeExpandServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExpandServiceServer will +// result in compilation errors. +type UnsafeExpandServiceServer interface { + mustEmbedUnimplementedExpandServiceServer() +} + +func RegisterExpandServiceServer(s grpc.ServiceRegistrar, srv ExpandServiceServer) { + // If the following call panics, it indicates UnimplementedExpandServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ExpandService_ServiceDesc, srv) +} + +func _ExpandService_Expand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExpandRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExpandServiceServer).Expand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExpandService_Expand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExpandServiceServer).Expand(ctx, req.(*ExpandRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ExpandService_ServiceDesc is the grpc.ServiceDesc for ExpandService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ExpandService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.ExpandService", + HandlerType: (*ExpandServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Expand", + Handler: _ExpandService_Expand_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/expand_service.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go new file mode 100644 index 0000000..4d84115 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go @@ -0,0 +1,219 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for ReadService.ListNamespaces RPC. +type ListNamespacesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNamespacesRequest) Reset() { + *x = ListNamespacesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNamespacesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNamespacesRequest) ProtoMessage() {} + +func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNamespacesRequest.ProtoReflect.Descriptor instead. +func (*ListNamespacesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{0} +} + +type ListNamespacesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespaces []*Namespace `protobuf:"bytes,1,rep,name=namespaces,proto3" json:"namespaces,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNamespacesResponse) Reset() { + *x = ListNamespacesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNamespacesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNamespacesResponse) ProtoMessage() {} + +func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNamespacesResponse.ProtoReflect.Descriptor instead. +func (*ListNamespacesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListNamespacesResponse) GetNamespaces() []*Namespace { + if x != nil { + return x.Namespaces + } + return nil +} + +type Namespace struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Namespace) Reset() { + *x = Namespace{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Namespace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Namespace) ProtoMessage() {} + +func (x *Namespace) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Namespace.ProtoReflect.Descriptor instead. +func (*Namespace) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{2} +} + +func (x *Namespace) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc = "" + + "\n" + + ":ory/keto/relation_tuples/v1alpha2/namespaces_service.proto\x12!ory.keto.relation_tuples.v1alpha2\"\x17\n" + + "\x15ListNamespacesRequest\"f\n" + + "\x16ListNamespacesResponse\x12L\n" + + "\n" + + "namespaces\x18\x01 \x03(\v2,.ory.keto.relation_tuples.v1alpha2.NamespaceR\n" + + "namespaces\"\x1f\n" + + "\tNamespace\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name2\x9b\x01\n" + + "\x11NamespacesService\x12\x85\x01\n" + + "\x0eListNamespaces\x128.ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest\x1a9.ory.keto.relation_tuples.v1alpha2.ListNamespacesResponseB\xaf\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x16NamespacesServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes = []any{ + (*ListNamespacesRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest + (*ListNamespacesResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse + (*Namespace)(nil), // 2: ory.keto.relation_tuples.v1alpha2.Namespace +} +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse.namespaces:type_name -> ory.keto.relation_tuples.v1alpha2.Namespace + 0, // 1: ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces:input_type -> ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest + 1, // 2: ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces:output_type -> ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go new file mode 100644 index 0000000..9de2177 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go @@ -0,0 +1,134 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NamespacesService_ListNamespaces_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.NamespacesService/ListNamespaces" +) + +// NamespacesServiceClient is the client API for NamespacesService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service to query namespaces. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type NamespacesServiceClient interface { + // Lists Namespaces + ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) +} + +type namespacesServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNamespacesServiceClient(cc grpc.ClientConnInterface) NamespacesServiceClient { + return &namespacesServiceClient{cc} +} + +func (c *namespacesServiceClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListNamespacesResponse) + err := c.cc.Invoke(ctx, NamespacesService_ListNamespaces_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NamespacesServiceServer is the server API for NamespacesService service. +// All implementations must embed UnimplementedNamespacesServiceServer +// for forward compatibility. +// +// The service to query namespaces. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type NamespacesServiceServer interface { + // Lists Namespaces + ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) + mustEmbedUnimplementedNamespacesServiceServer() +} + +// UnimplementedNamespacesServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNamespacesServiceServer struct{} + +func (UnimplementedNamespacesServiceServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListNamespaces not implemented") +} +func (UnimplementedNamespacesServiceServer) mustEmbedUnimplementedNamespacesServiceServer() {} +func (UnimplementedNamespacesServiceServer) testEmbeddedByValue() {} + +// UnsafeNamespacesServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NamespacesServiceServer will +// result in compilation errors. +type UnsafeNamespacesServiceServer interface { + mustEmbedUnimplementedNamespacesServiceServer() +} + +func RegisterNamespacesServiceServer(s grpc.ServiceRegistrar, srv NamespacesServiceServer) { + // If the following call panics, it indicates UnimplementedNamespacesServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NamespacesService_ServiceDesc, srv) +} + +func _NamespacesService_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListNamespacesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NamespacesServiceServer).ListNamespaces(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NamespacesService_ListNamespaces_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NamespacesServiceServer).ListNamespaces(ctx, req.(*ListNamespacesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NamespacesService_ServiceDesc is the grpc.ServiceDesc for NamespacesService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NamespacesService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.NamespacesService", + HandlerType: (*NamespacesServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListNamespaces", + Handler: _NamespacesService_ListNamespaces_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/namespaces_service.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/read_service.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/read_service.pb.go new file mode 100644 index 0000000..de73213 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/read_service.pb.go @@ -0,0 +1,371 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/read_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for ReadService.ListRelationTuples RPC. +// See `ListRelationTuplesRequest_Query` for how to filter the query. +type ListRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // All query constraints are concatenated + // with a logical AND operator. + // + // The RelationTuple list from ListRelationTuplesResponse + // is ordered from the newest RelationTuple to the oldest. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/read_service.proto. + Query *ListRelationTuplesRequest_Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + RelationQuery *RelationQuery `protobuf:"bytes,6,opt,name=relation_query,json=relationQuery,proto3" json:"relation_query,omitempty"` + // This field is not implemented yet and has no effect. + // + ExpandMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=expand_mask,json=expandMask,proto3" json:"expand_mask,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // Optional. The maximum number of + // RelationTuples to return in the response. + // + // Default: 100 + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. An opaque pagination token returned from + // a previous call to `ListRelationTuples` that + // indicates where the page should start at. + // + // An empty token denotes the first page. All successive + // pages require the token from the previous page. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesRequest) Reset() { + *x = ListRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesRequest) ProtoMessage() {} + +func (x *ListRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/read_service.proto. +func (x *ListRelationTuplesRequest) GetQuery() *ListRelationTuplesRequest_Query { + if x != nil { + return x.Query + } + return nil +} + +func (x *ListRelationTuplesRequest) GetRelationQuery() *RelationQuery { + if x != nil { + return x.RelationQuery + } + return nil +} + +func (x *ListRelationTuplesRequest) GetExpandMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.ExpandMask + } + return nil +} + +func (x *ListRelationTuplesRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *ListRelationTuplesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRelationTuplesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// The response of a ReadService.ListRelationTuples RPC. +type ListRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The relationships matching the list request. + RelationTuples []*RelationTuple `protobuf:"bytes,1,rep,name=relation_tuples,json=relationTuples,proto3" json:"relation_tuples,omitempty"` + // The token required to get the next page. + // If this is the last page, the token will be the empty string. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesResponse) Reset() { + *x = ListRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesResponse) ProtoMessage() {} + +func (x *ListRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListRelationTuplesResponse) GetRelationTuples() []*RelationTuple { + if x != nil { + return x.RelationTuples + } + return nil +} + +func (x *ListRelationTuplesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// The query for listing relationships. +// Clients can specify any optional field to +// partially filter for specific relationships. +// +// Example use cases (namespace is always required): +// - object only: display a list of all permissions referring to a specific object +// - relation only: get all groups that have members; get all directories that have content +// - object & relation: display all subjects that have a specific permission relation +// - subject & relation: display all groups a subject belongs to; display all objects a subject has access to +// - object & relation & subject: check whether the relation tuple already exists +type ListRelationTuplesRequest_Query struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The namespace to query. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Optional. The object to query for. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // Optional. The relation to query for. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // Optional. The subject to query for. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesRequest_Query) Reset() { + *x = ListRelationTuplesRequest_Query{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesRequest_Query) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesRequest_Query) ProtoMessage() {} + +func (x *ListRelationTuplesRequest_Query) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesRequest_Query.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesRequest_Query) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *ListRelationTuplesRequest_Query) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_read_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc = "" + + "\n" + + "4ory/keto/relation_tuples/v1alpha2/read_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\x1a google/protobuf/field_mask.proto\"\x8b\x04\n" + + "\x19ListRelationTuplesRequest\x12\\\n" + + "\x05query\x18\x01 \x01(\v2B.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.QueryB\x02\x18\x01R\x05query\x12W\n" + + "\x0erelation_query\x18\x06 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationQueryR\rrelationQuery\x12;\n" + + "\vexpand_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" + + "expandMask\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageToken\x1a\x9f\x01\n" + + "\x05Query\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\x9f\x01\n" + + "\x1aListRelationTuplesResponse\x12Y\n" + + "\x0frelation_tuples\x18\x01 \x03(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x0erelationTuples\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken2\xa1\x01\n" + + "\vReadService\x12\x91\x01\n" + + "\x12ListRelationTuples\x12<.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest\x1a=.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponseB\xa9\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x10ReadServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes = []any{ + (*ListRelationTuplesRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest + (*ListRelationTuplesResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse + (*ListRelationTuplesRequest_Query)(nil), // 2: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query + (*RelationQuery)(nil), // 3: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*fieldmaskpb.FieldMask)(nil), // 4: google.protobuf.FieldMask + (*RelationTuple)(nil), // 5: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*Subject)(nil), // 6: ory.keto.relation_tuples.v1alpha2.Subject +} +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.query:type_name -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query + 3, // 1: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.relation_query:type_name -> ory.keto.relation_tuples.v1alpha2.RelationQuery + 4, // 2: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.expand_mask:type_name -> google.protobuf.FieldMask + 5, // 3: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse.relation_tuples:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 4: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 0, // 5: ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest + 1, // 6: ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_read_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_read_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_read_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_read_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go new file mode 100644 index 0000000..f451ad8 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go @@ -0,0 +1,134 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/read_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ReadService_ListRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.ReadService/ListRelationTuples" +) + +// ReadServiceClient is the client API for ReadService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service to query relationships. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ReadServiceClient interface { + // Lists ACL relationships. + ListRelationTuples(ctx context.Context, in *ListRelationTuplesRequest, opts ...grpc.CallOption) (*ListRelationTuplesResponse, error) +} + +type readServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewReadServiceClient(cc grpc.ClientConnInterface) ReadServiceClient { + return &readServiceClient{cc} +} + +func (c *readServiceClient) ListRelationTuples(ctx context.Context, in *ListRelationTuplesRequest, opts ...grpc.CallOption) (*ListRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRelationTuplesResponse) + err := c.cc.Invoke(ctx, ReadService_ListRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ReadServiceServer is the server API for ReadService service. +// All implementations must embed UnimplementedReadServiceServer +// for forward compatibility. +// +// The service to query relationships. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ReadServiceServer interface { + // Lists ACL relationships. + ListRelationTuples(context.Context, *ListRelationTuplesRequest) (*ListRelationTuplesResponse, error) + mustEmbedUnimplementedReadServiceServer() +} + +// UnimplementedReadServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedReadServiceServer struct{} + +func (UnimplementedReadServiceServer) ListRelationTuples(context.Context, *ListRelationTuplesRequest) (*ListRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRelationTuples not implemented") +} +func (UnimplementedReadServiceServer) mustEmbedUnimplementedReadServiceServer() {} +func (UnimplementedReadServiceServer) testEmbeddedByValue() {} + +// UnsafeReadServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ReadServiceServer will +// result in compilation errors. +type UnsafeReadServiceServer interface { + mustEmbedUnimplementedReadServiceServer() +} + +func RegisterReadServiceServer(s grpc.ServiceRegistrar, srv ReadServiceServer) { + // If the following call panics, it indicates UnimplementedReadServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ReadService_ServiceDesc, srv) +} + +func _ReadService_ListRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReadServiceServer).ListRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReadService_ListRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReadServiceServer).ListRelationTuples(ctx, req.(*ListRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ReadService_ServiceDesc is the grpc.ServiceDesc for ReadService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ReadService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.ReadService", + HandlerType: (*ReadServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListRelationTuples", + Handler: _ReadService_ListRelationTuples_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/read_service.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go new file mode 100644 index 0000000..7757927 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go @@ -0,0 +1,432 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/relation_tuples.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RelationTuple defines a relation between an Object and a Subject. +type RelationTuple struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace this relation tuple lives in. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The object related by this tuple. + // It is an object in the namespace of the tuple. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between an Object and a Subject. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationTuple) Reset() { + *x = RelationTuple{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationTuple) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationTuple) ProtoMessage() {} + +func (x *RelationTuple) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationTuple.ProtoReflect.Descriptor instead. +func (*RelationTuple) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{0} +} + +func (x *RelationTuple) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RelationTuple) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *RelationTuple) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *RelationTuple) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +// The query for listing relationships. +// Clients can specify any optional field to +// partially filter for specific relationships. +// +// Example use cases (namespace is always required): +// - object only: display a list of all permissions referring to a specific object +// - relation only: get all groups that have members; get all directories that have content +// - object & relation: display all subjects that have a specific permission relation +// - subject & relation: display all groups a subject belongs to; display all objects a subject has access to +// - object & relation & subject: check whether the relation tuple already exists +type RelationQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace this relation tuple lives in. + Namespace *string `protobuf:"bytes,1,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` + // The object related by this tuple. + // It is an object in the namespace of the tuple. + Object *string `protobuf:"bytes,2,opt,name=object,proto3,oneof" json:"object,omitempty"` + // The relation between an Object and a Subject. + Relation *string `protobuf:"bytes,3,opt,name=relation,proto3,oneof" json:"relation,omitempty"` + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3,oneof" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationQuery) Reset() { + *x = RelationQuery{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationQuery) ProtoMessage() {} + +func (x *RelationQuery) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationQuery.ProtoReflect.Descriptor instead. +func (*RelationQuery) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{1} +} + +func (x *RelationQuery) GetNamespace() string { + if x != nil && x.Namespace != nil { + return *x.Namespace + } + return "" +} + +func (x *RelationQuery) GetObject() string { + if x != nil && x.Object != nil { + return *x.Object + } + return "" +} + +func (x *RelationQuery) GetRelation() string { + if x != nil && x.Relation != nil { + return *x.Relation + } + return "" +} + +func (x *RelationQuery) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +// Subject is either a concrete subject id or +// a `SubjectSet` expanding to more Subjects. +type Subject struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The reference of this abstract subject. + // + // Types that are valid to be assigned to Ref: + // + // *Subject_Id + // *Subject_Set + Ref isSubject_Ref `protobuf_oneof:"ref"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Subject) Reset() { + *x = Subject{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Subject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Subject) ProtoMessage() {} + +func (x *Subject) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Subject.ProtoReflect.Descriptor instead. +func (*Subject) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{2} +} + +func (x *Subject) GetRef() isSubject_Ref { + if x != nil { + return x.Ref + } + return nil +} + +func (x *Subject) GetId() string { + if x != nil { + if x, ok := x.Ref.(*Subject_Id); ok { + return x.Id + } + } + return "" +} + +func (x *Subject) GetSet() *SubjectSet { + if x != nil { + if x, ok := x.Ref.(*Subject_Set); ok { + return x.Set + } + } + return nil +} + +type isSubject_Ref interface { + isSubject_Ref() +} + +type Subject_Id struct { + // A concrete id of the subject. + Id string `protobuf:"bytes,1,opt,name=id,proto3,oneof"` +} + +type Subject_Set struct { + // A subject set that expands to more Subjects. + // More information are available under [concepts](../concepts/15_subjects.mdx). + Set *SubjectSet `protobuf:"bytes,2,opt,name=set,proto3,oneof"` +} + +func (*Subject_Id) isSubject_Ref() {} + +func (*Subject_Set) isSubject_Ref() {} + +// SubjectSet refers to all subjects who have +// the same `relation` on an `object`. +type SubjectSet struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace of the object and relation + // referenced in this subject set. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The object related by this subject set. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between the object and the subjects. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubjectSet) Reset() { + *x = SubjectSet{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubjectSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectSet) ProtoMessage() {} + +func (x *SubjectSet) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectSet.ProtoReflect.Descriptor instead. +func (*SubjectSet) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{3} +} + +func (x *SubjectSet) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *SubjectSet) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *SubjectSet) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc = "" + + "\n" + + "7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\x12!ory.keto.relation_tuples.v1alpha2\"\xa7\x01\n" + + "\rRelationTuple\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\xed\x01\n" + + "\rRelationQuery\x12!\n" + + "\tnamespace\x18\x01 \x01(\tH\x00R\tnamespace\x88\x01\x01\x12\x1b\n" + + "\x06object\x18\x02 \x01(\tH\x01R\x06object\x88\x01\x01\x12\x1f\n" + + "\brelation\x18\x03 \x01(\tH\x02R\brelation\x88\x01\x01\x12I\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectH\x03R\asubject\x88\x01\x01B\f\n" + + "\n" + + "_namespaceB\t\n" + + "\a_objectB\v\n" + + "\t_relationB\n" + + "\n" + + "\b_subject\"e\n" + + "\aSubject\x12\x10\n" + + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x12A\n" + + "\x03set\x18\x02 \x01(\v2-.ory.keto.relation_tuples.v1alpha2.SubjectSetH\x00R\x03setB\x05\n" + + "\x03ref\"^\n" + + "\n" + + "SubjectSet\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelationB\xac\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x13RelationTuplesProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes = []any{ + (*RelationTuple)(nil), // 0: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*RelationQuery)(nil), // 1: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*Subject)(nil), // 2: ory.keto.relation_tuples.v1alpha2.Subject + (*SubjectSet)(nil), // 3: ory.keto.relation_tuples.v1alpha2.SubjectSet +} +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.RelationTuple.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 2, // 1: ory.keto.relation_tuples.v1alpha2.RelationQuery.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 3, // 2: ory.keto.relation_tuples.v1alpha2.Subject.set:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectSet + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1].OneofWrappers = []any{} + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2].OneofWrappers = []any{ + (*Subject_Id)(nil), + (*Subject_Set)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/check_service.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/check_service.connect.go new file mode 100644 index 0000000..31d3cec --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/check_service.connect.go @@ -0,0 +1,142 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/check_service.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // CheckServiceName is the fully-qualified name of the CheckService service. + CheckServiceName = "ory.keto.relation_tuples.v1alpha2.CheckService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // CheckServiceCheckProcedure is the fully-qualified name of the CheckService's Check RPC. + CheckServiceCheckProcedure = "/ory.keto.relation_tuples.v1alpha2.CheckService/Check" + // CheckServiceBatchCheckProcedure is the fully-qualified name of the CheckService's BatchCheck RPC. + CheckServiceBatchCheckProcedure = "/ory.keto.relation_tuples.v1alpha2.CheckService/BatchCheck" +) + +// CheckServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.CheckService service. +type CheckServiceClient interface { + // Performs an authorization check. + Check(context.Context, *connect.Request[v1alpha2.CheckRequest]) (*connect.Response[v1alpha2.CheckResponse], error) + BatchCheck(context.Context, *connect.Request[v1alpha2.BatchCheckRequest]) (*connect.Response[v1alpha2.BatchCheckResponse], error) +} + +// NewCheckServiceClient constructs a client for the ory.keto.relation_tuples.v1alpha2.CheckService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewCheckServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) CheckServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + checkServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_check_service_proto.Services().ByName("CheckService").Methods() + return &checkServiceClient{ + check: connect.NewClient[v1alpha2.CheckRequest, v1alpha2.CheckResponse]( + httpClient, + baseURL+CheckServiceCheckProcedure, + connect.WithSchema(checkServiceMethods.ByName("Check")), + connect.WithClientOptions(opts...), + ), + batchCheck: connect.NewClient[v1alpha2.BatchCheckRequest, v1alpha2.BatchCheckResponse]( + httpClient, + baseURL+CheckServiceBatchCheckProcedure, + connect.WithSchema(checkServiceMethods.ByName("BatchCheck")), + connect.WithClientOptions(opts...), + ), + } +} + +// checkServiceClient implements CheckServiceClient. +type checkServiceClient struct { + check *connect.Client[v1alpha2.CheckRequest, v1alpha2.CheckResponse] + batchCheck *connect.Client[v1alpha2.BatchCheckRequest, v1alpha2.BatchCheckResponse] +} + +// Check calls ory.keto.relation_tuples.v1alpha2.CheckService.Check. +func (c *checkServiceClient) Check(ctx context.Context, req *connect.Request[v1alpha2.CheckRequest]) (*connect.Response[v1alpha2.CheckResponse], error) { + return c.check.CallUnary(ctx, req) +} + +// BatchCheck calls ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck. +func (c *checkServiceClient) BatchCheck(ctx context.Context, req *connect.Request[v1alpha2.BatchCheckRequest]) (*connect.Response[v1alpha2.BatchCheckResponse], error) { + return c.batchCheck.CallUnary(ctx, req) +} + +// CheckServiceHandler is an implementation of the ory.keto.relation_tuples.v1alpha2.CheckService +// service. +type CheckServiceHandler interface { + // Performs an authorization check. + Check(context.Context, *connect.Request[v1alpha2.CheckRequest]) (*connect.Response[v1alpha2.CheckResponse], error) + BatchCheck(context.Context, *connect.Request[v1alpha2.BatchCheckRequest]) (*connect.Response[v1alpha2.BatchCheckResponse], error) +} + +// NewCheckServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewCheckServiceHandler(svc CheckServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + checkServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_check_service_proto.Services().ByName("CheckService").Methods() + checkServiceCheckHandler := connect.NewUnaryHandler( + CheckServiceCheckProcedure, + svc.Check, + connect.WithSchema(checkServiceMethods.ByName("Check")), + connect.WithHandlerOptions(opts...), + ) + checkServiceBatchCheckHandler := connect.NewUnaryHandler( + CheckServiceBatchCheckProcedure, + svc.BatchCheck, + connect.WithSchema(checkServiceMethods.ByName("BatchCheck")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.CheckService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case CheckServiceCheckProcedure: + checkServiceCheckHandler.ServeHTTP(w, r) + case CheckServiceBatchCheckProcedure: + checkServiceBatchCheckHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedCheckServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedCheckServiceHandler struct{} + +func (UnimplementedCheckServiceHandler) Check(context.Context, *connect.Request[v1alpha2.CheckRequest]) (*connect.Response[v1alpha2.CheckResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.CheckService.Check is not implemented")) +} + +func (UnimplementedCheckServiceHandler) BatchCheck(context.Context, *connect.Request[v1alpha2.BatchCheckRequest]) (*connect.Response[v1alpha2.BatchCheckResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/expand_service.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/expand_service.connect.go new file mode 100644 index 0000000..0c47905 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/expand_service.connect.go @@ -0,0 +1,115 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/expand_service.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ExpandServiceName is the fully-qualified name of the ExpandService service. + ExpandServiceName = "ory.keto.relation_tuples.v1alpha2.ExpandService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ExpandServiceExpandProcedure is the fully-qualified name of the ExpandService's Expand RPC. + ExpandServiceExpandProcedure = "/ory.keto.relation_tuples.v1alpha2.ExpandService/Expand" +) + +// ExpandServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.ExpandService service. +type ExpandServiceClient interface { + // Expands the subject set into a tree of subjects. + Expand(context.Context, *connect.Request[v1alpha2.ExpandRequest]) (*connect.Response[v1alpha2.ExpandResponse], error) +} + +// NewExpandServiceClient constructs a client for the +// ory.keto.relation_tuples.v1alpha2.ExpandService service. By default, it uses the Connect protocol +// with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed requests. To +// use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or connect.WithGRPCWeb() +// options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewExpandServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ExpandServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + expandServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_expand_service_proto.Services().ByName("ExpandService").Methods() + return &expandServiceClient{ + expand: connect.NewClient[v1alpha2.ExpandRequest, v1alpha2.ExpandResponse]( + httpClient, + baseURL+ExpandServiceExpandProcedure, + connect.WithSchema(expandServiceMethods.ByName("Expand")), + connect.WithClientOptions(opts...), + ), + } +} + +// expandServiceClient implements ExpandServiceClient. +type expandServiceClient struct { + expand *connect.Client[v1alpha2.ExpandRequest, v1alpha2.ExpandResponse] +} + +// Expand calls ory.keto.relation_tuples.v1alpha2.ExpandService.Expand. +func (c *expandServiceClient) Expand(ctx context.Context, req *connect.Request[v1alpha2.ExpandRequest]) (*connect.Response[v1alpha2.ExpandResponse], error) { + return c.expand.CallUnary(ctx, req) +} + +// ExpandServiceHandler is an implementation of the ory.keto.relation_tuples.v1alpha2.ExpandService +// service. +type ExpandServiceHandler interface { + // Expands the subject set into a tree of subjects. + Expand(context.Context, *connect.Request[v1alpha2.ExpandRequest]) (*connect.Response[v1alpha2.ExpandResponse], error) +} + +// NewExpandServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewExpandServiceHandler(svc ExpandServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + expandServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_expand_service_proto.Services().ByName("ExpandService").Methods() + expandServiceExpandHandler := connect.NewUnaryHandler( + ExpandServiceExpandProcedure, + svc.Expand, + connect.WithSchema(expandServiceMethods.ByName("Expand")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.ExpandService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ExpandServiceExpandProcedure: + expandServiceExpandHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedExpandServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedExpandServiceHandler struct{} + +func (UnimplementedExpandServiceHandler) Expand(context.Context, *connect.Request[v1alpha2.ExpandRequest]) (*connect.Response[v1alpha2.ExpandResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.ExpandService.Expand is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/namespaces_service.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/namespaces_service.connect.go new file mode 100644 index 0000000..e4b1bf5 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/namespaces_service.connect.go @@ -0,0 +1,117 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // NamespacesServiceName is the fully-qualified name of the NamespacesService service. + NamespacesServiceName = "ory.keto.relation_tuples.v1alpha2.NamespacesService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // NamespacesServiceListNamespacesProcedure is the fully-qualified name of the NamespacesService's + // ListNamespaces RPC. + NamespacesServiceListNamespacesProcedure = "/ory.keto.relation_tuples.v1alpha2.NamespacesService/ListNamespaces" +) + +// NamespacesServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.NamespacesService +// service. +type NamespacesServiceClient interface { + // Lists Namespaces + ListNamespaces(context.Context, *connect.Request[v1alpha2.ListNamespacesRequest]) (*connect.Response[v1alpha2.ListNamespacesResponse], error) +} + +// NewNamespacesServiceClient constructs a client for the +// ory.keto.relation_tuples.v1alpha2.NamespacesService service. By default, it uses the Connect +// protocol with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed +// requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewNamespacesServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) NamespacesServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + namespacesServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto.Services().ByName("NamespacesService").Methods() + return &namespacesServiceClient{ + listNamespaces: connect.NewClient[v1alpha2.ListNamespacesRequest, v1alpha2.ListNamespacesResponse]( + httpClient, + baseURL+NamespacesServiceListNamespacesProcedure, + connect.WithSchema(namespacesServiceMethods.ByName("ListNamespaces")), + connect.WithClientOptions(opts...), + ), + } +} + +// namespacesServiceClient implements NamespacesServiceClient. +type namespacesServiceClient struct { + listNamespaces *connect.Client[v1alpha2.ListNamespacesRequest, v1alpha2.ListNamespacesResponse] +} + +// ListNamespaces calls ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces. +func (c *namespacesServiceClient) ListNamespaces(ctx context.Context, req *connect.Request[v1alpha2.ListNamespacesRequest]) (*connect.Response[v1alpha2.ListNamespacesResponse], error) { + return c.listNamespaces.CallUnary(ctx, req) +} + +// NamespacesServiceHandler is an implementation of the +// ory.keto.relation_tuples.v1alpha2.NamespacesService service. +type NamespacesServiceHandler interface { + // Lists Namespaces + ListNamespaces(context.Context, *connect.Request[v1alpha2.ListNamespacesRequest]) (*connect.Response[v1alpha2.ListNamespacesResponse], error) +} + +// NewNamespacesServiceHandler builds an HTTP handler from the service implementation. It returns +// the path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewNamespacesServiceHandler(svc NamespacesServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + namespacesServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto.Services().ByName("NamespacesService").Methods() + namespacesServiceListNamespacesHandler := connect.NewUnaryHandler( + NamespacesServiceListNamespacesProcedure, + svc.ListNamespaces, + connect.WithSchema(namespacesServiceMethods.ByName("ListNamespaces")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.NamespacesService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case NamespacesServiceListNamespacesProcedure: + namespacesServiceListNamespacesHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedNamespacesServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedNamespacesServiceHandler struct{} + +func (UnimplementedNamespacesServiceHandler) ListNamespaces(context.Context, *connect.Request[v1alpha2.ListNamespacesRequest]) (*connect.Response[v1alpha2.ListNamespacesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/read_service.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/read_service.connect.go new file mode 100644 index 0000000..30ace20 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/read_service.connect.go @@ -0,0 +1,115 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/read_service.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ReadServiceName is the fully-qualified name of the ReadService service. + ReadServiceName = "ory.keto.relation_tuples.v1alpha2.ReadService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ReadServiceListRelationTuplesProcedure is the fully-qualified name of the ReadService's + // ListRelationTuples RPC. + ReadServiceListRelationTuplesProcedure = "/ory.keto.relation_tuples.v1alpha2.ReadService/ListRelationTuples" +) + +// ReadServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.ReadService service. +type ReadServiceClient interface { + // Lists ACL relationships. + ListRelationTuples(context.Context, *connect.Request[v1alpha2.ListRelationTuplesRequest]) (*connect.Response[v1alpha2.ListRelationTuplesResponse], error) +} + +// NewReadServiceClient constructs a client for the ory.keto.relation_tuples.v1alpha2.ReadService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewReadServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ReadServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + readServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_read_service_proto.Services().ByName("ReadService").Methods() + return &readServiceClient{ + listRelationTuples: connect.NewClient[v1alpha2.ListRelationTuplesRequest, v1alpha2.ListRelationTuplesResponse]( + httpClient, + baseURL+ReadServiceListRelationTuplesProcedure, + connect.WithSchema(readServiceMethods.ByName("ListRelationTuples")), + connect.WithClientOptions(opts...), + ), + } +} + +// readServiceClient implements ReadServiceClient. +type readServiceClient struct { + listRelationTuples *connect.Client[v1alpha2.ListRelationTuplesRequest, v1alpha2.ListRelationTuplesResponse] +} + +// ListRelationTuples calls ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples. +func (c *readServiceClient) ListRelationTuples(ctx context.Context, req *connect.Request[v1alpha2.ListRelationTuplesRequest]) (*connect.Response[v1alpha2.ListRelationTuplesResponse], error) { + return c.listRelationTuples.CallUnary(ctx, req) +} + +// ReadServiceHandler is an implementation of the ory.keto.relation_tuples.v1alpha2.ReadService +// service. +type ReadServiceHandler interface { + // Lists ACL relationships. + ListRelationTuples(context.Context, *connect.Request[v1alpha2.ListRelationTuplesRequest]) (*connect.Response[v1alpha2.ListRelationTuplesResponse], error) +} + +// NewReadServiceHandler builds an HTTP handler from the service implementation. It returns the path +// on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewReadServiceHandler(svc ReadServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + readServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_read_service_proto.Services().ByName("ReadService").Methods() + readServiceListRelationTuplesHandler := connect.NewUnaryHandler( + ReadServiceListRelationTuplesProcedure, + svc.ListRelationTuples, + connect.WithSchema(readServiceMethods.ByName("ListRelationTuples")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.ReadService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ReadServiceListRelationTuplesProcedure: + readServiceListRelationTuplesHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedReadServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedReadServiceHandler struct{} + +func (UnimplementedReadServiceHandler) ListRelationTuples(context.Context, *connect.Request[v1alpha2.ListRelationTuplesRequest]) (*connect.Response[v1alpha2.ListRelationTuplesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/version.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/version.connect.go new file mode 100644 index 0000000..a817d72 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/version.connect.go @@ -0,0 +1,117 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/version.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // VersionServiceName is the fully-qualified name of the VersionService service. + VersionServiceName = "ory.keto.relation_tuples.v1alpha2.VersionService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // VersionServiceGetVersionProcedure is the fully-qualified name of the VersionService's GetVersion + // RPC. + VersionServiceGetVersionProcedure = "/ory.keto.relation_tuples.v1alpha2.VersionService/GetVersion" +) + +// VersionServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.VersionService +// service. +type VersionServiceClient interface { + // Returns the version of the Ory Keto instance. + GetVersion(context.Context, *connect.Request[v1alpha2.GetVersionRequest]) (*connect.Response[v1alpha2.GetVersionResponse], error) +} + +// NewVersionServiceClient constructs a client for the +// ory.keto.relation_tuples.v1alpha2.VersionService service. By default, it uses the Connect +// protocol with the binary Protobuf Codec, asks for gzipped responses, and sends uncompressed +// requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or +// connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewVersionServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) VersionServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + versionServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_version_proto.Services().ByName("VersionService").Methods() + return &versionServiceClient{ + getVersion: connect.NewClient[v1alpha2.GetVersionRequest, v1alpha2.GetVersionResponse]( + httpClient, + baseURL+VersionServiceGetVersionProcedure, + connect.WithSchema(versionServiceMethods.ByName("GetVersion")), + connect.WithClientOptions(opts...), + ), + } +} + +// versionServiceClient implements VersionServiceClient. +type versionServiceClient struct { + getVersion *connect.Client[v1alpha2.GetVersionRequest, v1alpha2.GetVersionResponse] +} + +// GetVersion calls ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion. +func (c *versionServiceClient) GetVersion(ctx context.Context, req *connect.Request[v1alpha2.GetVersionRequest]) (*connect.Response[v1alpha2.GetVersionResponse], error) { + return c.getVersion.CallUnary(ctx, req) +} + +// VersionServiceHandler is an implementation of the +// ory.keto.relation_tuples.v1alpha2.VersionService service. +type VersionServiceHandler interface { + // Returns the version of the Ory Keto instance. + GetVersion(context.Context, *connect.Request[v1alpha2.GetVersionRequest]) (*connect.Response[v1alpha2.GetVersionResponse], error) +} + +// NewVersionServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewVersionServiceHandler(svc VersionServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + versionServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_version_proto.Services().ByName("VersionService").Methods() + versionServiceGetVersionHandler := connect.NewUnaryHandler( + VersionServiceGetVersionProcedure, + svc.GetVersion, + connect.WithSchema(versionServiceMethods.ByName("GetVersion")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.VersionService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case VersionServiceGetVersionProcedure: + versionServiceGetVersionHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedVersionServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedVersionServiceHandler struct{} + +func (UnimplementedVersionServiceHandler) GetVersion(context.Context, *connect.Request[v1alpha2.GetVersionRequest]) (*connect.Response[v1alpha2.GetVersionResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/write_service.connect.go b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/write_service.connect.go new file mode 100644 index 0000000..b30db16 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect/write_service.connect.go @@ -0,0 +1,147 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: ory/keto/relation_tuples/v1alpha2/write_service.proto + +package relationtuplesconnect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1alpha2 "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // WriteServiceName is the fully-qualified name of the WriteService service. + WriteServiceName = "ory.keto.relation_tuples.v1alpha2.WriteService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // WriteServiceTransactRelationTuplesProcedure is the fully-qualified name of the WriteService's + // TransactRelationTuples RPC. + WriteServiceTransactRelationTuplesProcedure = "/ory.keto.relation_tuples.v1alpha2.WriteService/TransactRelationTuples" + // WriteServiceDeleteRelationTuplesProcedure is the fully-qualified name of the WriteService's + // DeleteRelationTuples RPC. + WriteServiceDeleteRelationTuplesProcedure = "/ory.keto.relation_tuples.v1alpha2.WriteService/DeleteRelationTuples" +) + +// WriteServiceClient is a client for the ory.keto.relation_tuples.v1alpha2.WriteService service. +type WriteServiceClient interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(context.Context, *connect.Request[v1alpha2.TransactRelationTuplesRequest]) (*connect.Response[v1alpha2.TransactRelationTuplesResponse], error) + // Deletes relationships based on relation query + DeleteRelationTuples(context.Context, *connect.Request[v1alpha2.DeleteRelationTuplesRequest]) (*connect.Response[v1alpha2.DeleteRelationTuplesResponse], error) +} + +// NewWriteServiceClient constructs a client for the ory.keto.relation_tuples.v1alpha2.WriteService +// service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for +// gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply +// the connect.WithGRPC() or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewWriteServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) WriteServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + writeServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_write_service_proto.Services().ByName("WriteService").Methods() + return &writeServiceClient{ + transactRelationTuples: connect.NewClient[v1alpha2.TransactRelationTuplesRequest, v1alpha2.TransactRelationTuplesResponse]( + httpClient, + baseURL+WriteServiceTransactRelationTuplesProcedure, + connect.WithSchema(writeServiceMethods.ByName("TransactRelationTuples")), + connect.WithClientOptions(opts...), + ), + deleteRelationTuples: connect.NewClient[v1alpha2.DeleteRelationTuplesRequest, v1alpha2.DeleteRelationTuplesResponse]( + httpClient, + baseURL+WriteServiceDeleteRelationTuplesProcedure, + connect.WithSchema(writeServiceMethods.ByName("DeleteRelationTuples")), + connect.WithClientOptions(opts...), + ), + } +} + +// writeServiceClient implements WriteServiceClient. +type writeServiceClient struct { + transactRelationTuples *connect.Client[v1alpha2.TransactRelationTuplesRequest, v1alpha2.TransactRelationTuplesResponse] + deleteRelationTuples *connect.Client[v1alpha2.DeleteRelationTuplesRequest, v1alpha2.DeleteRelationTuplesResponse] +} + +// TransactRelationTuples calls +// ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples. +func (c *writeServiceClient) TransactRelationTuples(ctx context.Context, req *connect.Request[v1alpha2.TransactRelationTuplesRequest]) (*connect.Response[v1alpha2.TransactRelationTuplesResponse], error) { + return c.transactRelationTuples.CallUnary(ctx, req) +} + +// DeleteRelationTuples calls ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples. +func (c *writeServiceClient) DeleteRelationTuples(ctx context.Context, req *connect.Request[v1alpha2.DeleteRelationTuplesRequest]) (*connect.Response[v1alpha2.DeleteRelationTuplesResponse], error) { + return c.deleteRelationTuples.CallUnary(ctx, req) +} + +// WriteServiceHandler is an implementation of the ory.keto.relation_tuples.v1alpha2.WriteService +// service. +type WriteServiceHandler interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(context.Context, *connect.Request[v1alpha2.TransactRelationTuplesRequest]) (*connect.Response[v1alpha2.TransactRelationTuplesResponse], error) + // Deletes relationships based on relation query + DeleteRelationTuples(context.Context, *connect.Request[v1alpha2.DeleteRelationTuplesRequest]) (*connect.Response[v1alpha2.DeleteRelationTuplesResponse], error) +} + +// NewWriteServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewWriteServiceHandler(svc WriteServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + writeServiceMethods := v1alpha2.File_ory_keto_relation_tuples_v1alpha2_write_service_proto.Services().ByName("WriteService").Methods() + writeServiceTransactRelationTuplesHandler := connect.NewUnaryHandler( + WriteServiceTransactRelationTuplesProcedure, + svc.TransactRelationTuples, + connect.WithSchema(writeServiceMethods.ByName("TransactRelationTuples")), + connect.WithHandlerOptions(opts...), + ) + writeServiceDeleteRelationTuplesHandler := connect.NewUnaryHandler( + WriteServiceDeleteRelationTuplesProcedure, + svc.DeleteRelationTuples, + connect.WithSchema(writeServiceMethods.ByName("DeleteRelationTuples")), + connect.WithHandlerOptions(opts...), + ) + return "/ory.keto.relation_tuples.v1alpha2.WriteService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case WriteServiceTransactRelationTuplesProcedure: + writeServiceTransactRelationTuplesHandler.ServeHTTP(w, r) + case WriteServiceDeleteRelationTuplesProcedure: + writeServiceDeleteRelationTuplesHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedWriteServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedWriteServiceHandler struct{} + +func (UnimplementedWriteServiceHandler) TransactRelationTuples(context.Context, *connect.Request[v1alpha2.TransactRelationTuplesRequest]) (*connect.Response[v1alpha2.TransactRelationTuplesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples is not implemented")) +} + +func (UnimplementedWriteServiceHandler) DeleteRelationTuples(context.Context, *connect.Request[v1alpha2.DeleteRelationTuplesRequest]) (*connect.Response[v1alpha2.DeleteRelationTuplesResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples is not implemented")) +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/utils.go b/gen/go/ory/keto/relation_tuples/v1alpha2/utils.go new file mode 100644 index 0000000..dadd076 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/utils.go @@ -0,0 +1,36 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuples + +// RelationTupleToDeltas is a helper that converts a slice of RelationTuple to a slice of RelationTupleDelta +// with the specified RelationTupleDelta_Action. This allows you to conveniently assemble a request for the +// WriteServiceClient.TransactRelationTuples operation. +// +// Example: +// +// c.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ +// RelationTupleDeltas: append(rts.RelationTupleToDeltas(insertTuples, rts.RelationTupleDelta_ACTION_INSERT), rts.RelationTupleToDeltas(deleteTuples, rts.RelationTupleDelta_ACTION_DELETE)...), +// }) +func RelationTupleToDeltas(rs []*RelationTuple, action RelationTupleDelta_Action) []*RelationTupleDelta { + deltas := make([]*RelationTupleDelta, len(rs)) + for i := range rs { + deltas[i] = &RelationTupleDelta{ + RelationTuple: rs[i], + Action: action, + } + } + return deltas +} + +// NewSubjectSet returns a Subject with a SubjectSet ref. +func NewSubjectSet(namespace, object, relation string) *Subject { + return &Subject{Ref: &Subject_Set{Set: &SubjectSet{ + Namespace: namespace, + Object: object, + Relation: relation, + }}} +} + +// NewSubjectID returns a Subject with a subject ID ref. +func NewSubjectID(id string) *Subject { return &Subject{Ref: &Subject_Id{Id: id}} } diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/utils_test.go b/gen/go/ory/keto/relation_tuples/v1alpha2/utils_test.go new file mode 100644 index 0000000..0c64002 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/utils_test.go @@ -0,0 +1,67 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuples + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRelationTupleToDeltas(t *testing.T) { + for _, tc := range []struct { + desc string + rts []*RelationTuple + action RelationTupleDelta_Action + expected []*RelationTupleDelta + }{ + { + desc: "empty list", + rts: nil, + action: RelationTupleDelta_ACTION_INSERT, + expected: []*RelationTupleDelta{}, + }, + { + desc: "some tuples", + rts: []*RelationTuple{ + { + Namespace: "n", + Object: "o1", + Relation: "r", + Subject: NewSubjectID("s"), + }, { + Namespace: "n", + Object: "o2", + Relation: "r", + Subject: NewSubjectSet("sn", "so", "sr"), + }, + }, + action: RelationTupleDelta_ACTION_DELETE, + expected: []*RelationTupleDelta{ + { + RelationTuple: &RelationTuple{ + Namespace: "n", + Object: "o1", + Relation: "r", + Subject: NewSubjectID("s"), + }, + Action: RelationTupleDelta_ACTION_DELETE, + }, + { + RelationTuple: &RelationTuple{ + Namespace: "n", + Object: "o2", + Relation: "r", + Subject: NewSubjectSet("sn", "so", "sr"), + }, + Action: RelationTupleDelta_ACTION_DELETE, + }, + }, + }, + } { + t.Run("case="+tc.desc, func(t *testing.T) { + assert.Equal(t, tc.expected, RelationTupleToDeltas(tc.rts, tc.action)) + }) + } +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/version.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/version.pb.go new file mode 100644 index 0000000..8c114ca --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/version.pb.go @@ -0,0 +1,172 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/version.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for the VersionService.GetVersion RPC. +type GetVersionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionRequest) Reset() { + *x = GetVersionRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionRequest) ProtoMessage() {} + +func (x *GetVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVersionRequest.ProtoReflect.Descriptor instead. +func (*GetVersionRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP(), []int{0} +} + +// Response of the VersionService.GetVersion RPC. +type GetVersionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The version string of the Ory Keto instance. + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionResponse) Reset() { + *x = GetVersionResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionResponse) ProtoMessage() {} + +func (x *GetVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVersionResponse.ProtoReflect.Descriptor instead. +func (*GetVersionResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP(), []int{1} +} + +func (x *GetVersionResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_version_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc = "" + + "\n" + + "/ory/keto/relation_tuples/v1alpha2/version.proto\x12!ory.keto.relation_tuples.v1alpha2\"\x13\n" + + "\x11GetVersionRequest\".\n" + + "\x12GetVersionResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion2\x8b\x01\n" + + "\x0eVersionService\x12y\n" + + "\n" + + "GetVersion\x124.ory.keto.relation_tuples.v1alpha2.GetVersionRequest\x1a5.ory.keto.relation_tuples.v1alpha2.GetVersionResponseB\xa5\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\fVersionProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes = []any{ + (*GetVersionRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.GetVersionRequest + (*GetVersionResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.GetVersionResponse +} +var file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs = []int32{ + 0, // 0: ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion:input_type -> ory.keto.relation_tuples.v1alpha2.GetVersionRequest + 1, // 1: ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion:output_type -> ory.keto.relation_tuples.v1alpha2.GetVersionResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_version_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_version_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_version_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_version_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go new file mode 100644 index 0000000..09fe13c --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go @@ -0,0 +1,134 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/version.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + VersionService_GetVersion_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.VersionService/GetVersion" +) + +// VersionServiceClient is the client API for VersionService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service returning the specific Ory Keto instance version. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type VersionServiceClient interface { + // Returns the version of the Ory Keto instance. + GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) +} + +type versionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewVersionServiceClient(cc grpc.ClientConnInterface) VersionServiceClient { + return &versionServiceClient{cc} +} + +func (c *versionServiceClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetVersionResponse) + err := c.cc.Invoke(ctx, VersionService_GetVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VersionServiceServer is the server API for VersionService service. +// All implementations must embed UnimplementedVersionServiceServer +// for forward compatibility. +// +// The service returning the specific Ory Keto instance version. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type VersionServiceServer interface { + // Returns the version of the Ory Keto instance. + GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) + mustEmbedUnimplementedVersionServiceServer() +} + +// UnimplementedVersionServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedVersionServiceServer struct{} + +func (UnimplementedVersionServiceServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetVersion not implemented") +} +func (UnimplementedVersionServiceServer) mustEmbedUnimplementedVersionServiceServer() {} +func (UnimplementedVersionServiceServer) testEmbeddedByValue() {} + +// UnsafeVersionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VersionServiceServer will +// result in compilation errors. +type UnsafeVersionServiceServer interface { + mustEmbedUnimplementedVersionServiceServer() +} + +func RegisterVersionServiceServer(s grpc.ServiceRegistrar, srv VersionServiceServer) { + // If the following call panics, it indicates UnimplementedVersionServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&VersionService_ServiceDesc, srv) +} + +func _VersionService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VersionServiceServer).GetVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VersionService_GetVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VersionServiceServer).GetVersion(ctx, req.(*GetVersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// VersionService_ServiceDesc is the grpc.ServiceDesc for VersionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var VersionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.VersionService", + HandlerType: (*VersionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetVersion", + Handler: _VersionService_GetVersion_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/version.proto", +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/write_service.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/write_service.pb.go new file mode 100644 index 0000000..947308a --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/write_service.pb.go @@ -0,0 +1,501 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/write_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RelationTupleDelta_Action int32 + +const ( + // Unspecified. + // The `TransactRelationTuples` RPC ignores this + // RelationTupleDelta if an action was unspecified. + RelationTupleDelta_ACTION_UNSPECIFIED RelationTupleDelta_Action = 0 + // Insertion of a new RelationTuple. + RelationTupleDelta_ACTION_INSERT RelationTupleDelta_Action = 1 + // Deletion of the RelationTuple. + // It is ignored if it does not exist. + RelationTupleDelta_ACTION_DELETE RelationTupleDelta_Action = 2 +) + +// Enum value maps for RelationTupleDelta_Action. +var ( + RelationTupleDelta_Action_name = map[int32]string{ + 0: "ACTION_UNSPECIFIED", + 1: "ACTION_INSERT", + 2: "ACTION_DELETE", + } + RelationTupleDelta_Action_value = map[string]int32{ + "ACTION_UNSPECIFIED": 0, + "ACTION_INSERT": 1, + "ACTION_DELETE": 2, + } +) + +func (x RelationTupleDelta_Action) Enum() *RelationTupleDelta_Action { + p := new(RelationTupleDelta_Action) + *p = x + return p +} + +func (x RelationTupleDelta_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RelationTupleDelta_Action) Descriptor() protoreflect.EnumDescriptor { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes[0].Descriptor() +} + +func (RelationTupleDelta_Action) Type() protoreflect.EnumType { + return &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes[0] +} + +func (x RelationTupleDelta_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RelationTupleDelta_Action.Descriptor instead. +func (RelationTupleDelta_Action) EnumDescriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{1, 0} +} + +// The request of a WriteService.TransactRelationTuples RPC. +type TransactRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The write delta for the relationships operated in one single transaction. + // Either all actions succeed or no change takes effect on error. + RelationTupleDeltas []*RelationTupleDelta `protobuf:"bytes,1,rep,name=relation_tuple_deltas,json=relationTupleDeltas,proto3" json:"relation_tuple_deltas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactRelationTuplesRequest) Reset() { + *x = TransactRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactRelationTuplesRequest) ProtoMessage() {} + +func (x *TransactRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransactRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*TransactRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{0} +} + +func (x *TransactRelationTuplesRequest) GetRelationTupleDeltas() []*RelationTupleDelta { + if x != nil { + return x.RelationTupleDeltas + } + return nil +} + +// Write-delta for a TransactRelationTuplesRequest. +type RelationTupleDelta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The action to do on the RelationTuple. + Action RelationTupleDelta_Action `protobuf:"varint,1,opt,name=action,proto3,enum=ory.keto.relation_tuples.v1alpha2.RelationTupleDelta_Action" json:"action,omitempty"` + // The target RelationTuple. + RelationTuple *RelationTuple `protobuf:"bytes,2,opt,name=relation_tuple,json=relationTuple,proto3" json:"relation_tuple,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationTupleDelta) Reset() { + *x = RelationTupleDelta{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationTupleDelta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationTupleDelta) ProtoMessage() {} + +func (x *RelationTupleDelta) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationTupleDelta.ProtoReflect.Descriptor instead. +func (*RelationTupleDelta) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{1} +} + +func (x *RelationTupleDelta) GetAction() RelationTupleDelta_Action { + if x != nil { + return x.Action + } + return RelationTupleDelta_ACTION_UNSPECIFIED +} + +func (x *RelationTupleDelta) GetRelationTuple() *RelationTuple { + if x != nil { + return x.RelationTuple + } + return nil +} + +// The response of a WriteService.TransactRelationTuples rpc. +type TransactRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This field is not implemented yet and has no effect. + // + Snaptokens []string `protobuf:"bytes,1,rep,name=snaptokens,proto3" json:"snaptokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactRelationTuplesResponse) Reset() { + *x = TransactRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactRelationTuplesResponse) ProtoMessage() {} + +func (x *TransactRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransactRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*TransactRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{2} +} + +func (x *TransactRelationTuplesResponse) GetSnaptokens() []string { + if x != nil { + return x.Snaptokens + } + return nil +} + +type DeleteRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/write_service.proto. + Query *DeleteRelationTuplesRequest_Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + RelationQuery *RelationQuery `protobuf:"bytes,2,opt,name=relation_query,json=relationQuery,proto3" json:"relation_query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesRequest) Reset() { + *x = DeleteRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesRequest) ProtoMessage() {} + +func (x *DeleteRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{3} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/write_service.proto. +func (x *DeleteRelationTuplesRequest) GetQuery() *DeleteRelationTuplesRequest_Query { + if x != nil { + return x.Query + } + return nil +} + +func (x *DeleteRelationTuplesRequest) GetRelationQuery() *RelationQuery { + if x != nil { + return x.RelationQuery + } + return nil +} + +type DeleteRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesResponse) Reset() { + *x = DeleteRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesResponse) ProtoMessage() {} + +func (x *DeleteRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{4} +} + +// The query for deleting relationships +type DeleteRelationTuplesRequest_Query struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The namespace to query. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Optional. The object to query for. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // Optional. The relation to query for. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // Optional. The subject to query for. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesRequest_Query) Reset() { + *x = DeleteRelationTuplesRequest_Query{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesRequest_Query) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesRequest_Query) ProtoMessage() {} + +func (x *DeleteRelationTuplesRequest_Query) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesRequest_Query.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesRequest_Query) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *DeleteRelationTuplesRequest_Query) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_write_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc = "" + + "\n" + + "5ory/keto/relation_tuples/v1alpha2/write_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\x8a\x01\n" + + "\x1dTransactRelationTuplesRequest\x12i\n" + + "\x15relation_tuple_deltas\x18\x01 \x03(\v25.ory.keto.relation_tuples.v1alpha2.RelationTupleDeltaR\x13relationTupleDeltas\"\x8b\x02\n" + + "\x12RelationTupleDelta\x12T\n" + + "\x06action\x18\x01 \x01(\x0e2<.ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.ActionR\x06action\x12W\n" + + "\x0erelation_tuple\x18\x02 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\rrelationTuple\"F\n" + + "\x06Action\x12\x16\n" + + "\x12ACTION_UNSPECIFIED\x10\x00\x12\x11\n" + + "\rACTION_INSERT\x10\x01\x12\x11\n" + + "\rACTION_DELETE\x10\x02\"@\n" + + "\x1eTransactRelationTuplesResponse\x12\x1e\n" + + "\n" + + "snaptokens\x18\x01 \x03(\tR\n" + + "snaptokens\"\xf8\x02\n" + + "\x1bDeleteRelationTuplesRequest\x12^\n" + + "\x05query\x18\x01 \x01(\v2D.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.QueryB\x02\x18\x01R\x05query\x12W\n" + + "\x0erelation_query\x18\x02 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationQueryR\rrelationQuery\x1a\x9f\x01\n" + + "\x05Query\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\x1e\n" + + "\x1cDeleteRelationTuplesResponse2\xc8\x02\n" + + "\fWriteService\x12\x9d\x01\n" + + "\x16TransactRelationTuples\x12@.ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest\x1aA.ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse\x12\x97\x01\n" + + "\x14DeleteRelationTuples\x12>.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest\x1a?.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponseB\xaa\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x11WriteServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes = []any{ + (RelationTupleDelta_Action)(0), // 0: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + (*TransactRelationTuplesRequest)(nil), // 1: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest + (*RelationTupleDelta)(nil), // 2: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta + (*TransactRelationTuplesResponse)(nil), // 3: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse + (*DeleteRelationTuplesRequest)(nil), // 4: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest + (*DeleteRelationTuplesResponse)(nil), // 5: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse + (*DeleteRelationTuplesRequest_Query)(nil), // 6: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query + (*RelationTuple)(nil), // 7: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*RelationQuery)(nil), // 8: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*Subject)(nil), // 9: ory.keto.relation_tuples.v1alpha2.Subject +} +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest.relation_tuple_deltas:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTupleDelta + 0, // 1: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.action:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + 7, // 2: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.relation_tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 3: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.query:type_name -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query + 8, // 4: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.relation_query:type_name -> ory.keto.relation_tuples.v1alpha2.RelationQuery + 9, // 5: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 1, // 6: ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest + 4, // 7: ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest + 3, // 8: ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse + 5, // 9: ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_write_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_write_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_write_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs, + EnumInfos: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_write_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs = nil +} diff --git a/gen/go/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go b/gen/go/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go new file mode 100644 index 0000000..3893e98 --- /dev/null +++ b/gen/go/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go @@ -0,0 +1,174 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/write_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + WriteService_TransactRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.WriteService/TransactRelationTuples" + WriteService_DeleteRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.WriteService/DeleteRelationTuples" +) + +// WriteServiceClient is the client API for WriteService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The write service to create and delete Access Control Lists. +// +// This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type WriteServiceClient interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(ctx context.Context, in *TransactRelationTuplesRequest, opts ...grpc.CallOption) (*TransactRelationTuplesResponse, error) + // Deletes relationships based on relation query + DeleteRelationTuples(ctx context.Context, in *DeleteRelationTuplesRequest, opts ...grpc.CallOption) (*DeleteRelationTuplesResponse, error) +} + +type writeServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWriteServiceClient(cc grpc.ClientConnInterface) WriteServiceClient { + return &writeServiceClient{cc} +} + +func (c *writeServiceClient) TransactRelationTuples(ctx context.Context, in *TransactRelationTuplesRequest, opts ...grpc.CallOption) (*TransactRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransactRelationTuplesResponse) + err := c.cc.Invoke(ctx, WriteService_TransactRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *writeServiceClient) DeleteRelationTuples(ctx context.Context, in *DeleteRelationTuplesRequest, opts ...grpc.CallOption) (*DeleteRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteRelationTuplesResponse) + err := c.cc.Invoke(ctx, WriteService_DeleteRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WriteServiceServer is the server API for WriteService service. +// All implementations must embed UnimplementedWriteServiceServer +// for forward compatibility. +// +// The write service to create and delete Access Control Lists. +// +// This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type WriteServiceServer interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(context.Context, *TransactRelationTuplesRequest) (*TransactRelationTuplesResponse, error) + // Deletes relationships based on relation query + DeleteRelationTuples(context.Context, *DeleteRelationTuplesRequest) (*DeleteRelationTuplesResponse, error) + mustEmbedUnimplementedWriteServiceServer() +} + +// UnimplementedWriteServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWriteServiceServer struct{} + +func (UnimplementedWriteServiceServer) TransactRelationTuples(context.Context, *TransactRelationTuplesRequest) (*TransactRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TransactRelationTuples not implemented") +} +func (UnimplementedWriteServiceServer) DeleteRelationTuples(context.Context, *DeleteRelationTuplesRequest) (*DeleteRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRelationTuples not implemented") +} +func (UnimplementedWriteServiceServer) mustEmbedUnimplementedWriteServiceServer() {} +func (UnimplementedWriteServiceServer) testEmbeddedByValue() {} + +// UnsafeWriteServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WriteServiceServer will +// result in compilation errors. +type UnsafeWriteServiceServer interface { + mustEmbedUnimplementedWriteServiceServer() +} + +func RegisterWriteServiceServer(s grpc.ServiceRegistrar, srv WriteServiceServer) { + // If the following call panics, it indicates UnimplementedWriteServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WriteService_ServiceDesc, srv) +} + +func _WriteService_TransactRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransactRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WriteServiceServer).TransactRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WriteService_TransactRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WriteServiceServer).TransactRelationTuples(ctx, req.(*TransactRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WriteService_DeleteRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WriteServiceServer).DeleteRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WriteService_DeleteRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WriteServiceServer).DeleteRelationTuples(ctx, req.(*DeleteRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WriteService_ServiceDesc is the grpc.ServiceDesc for WriteService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WriteService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.WriteService", + HandlerType: (*WriteServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TransactRelationTuples", + Handler: _WriteService_TransactRelationTuples_Handler, + }, + { + MethodName: "DeleteRelationTuples", + Handler: _WriteService_DeleteRelationTuples_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/write_service.proto", +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1a592ad --- /dev/null +++ b/go.mod @@ -0,0 +1,301 @@ +go 1.26 + +module github.com/ory/keto + +replace ( + github.com/ory/keto/gen/go => ./gen/go + github.com/ory/x => ./oryx +) + +require ( + connectrpc.com/connect v1.20.0 + connectrpc.com/grpchealth v1.4.1-0.20260420170649-371aedad91aa + connectrpc.com/grpcreflect v1.3.0 + github.com/cenkalti/backoff/v3 v3.2.2 + github.com/dgraph-io/ristretto/v2 v2.4.0 + github.com/ghodss/yaml v1.0.0 + github.com/go-sql-driver/mysql v1.10.0 + github.com/gobuffalo/httptest v1.5.2 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 + github.com/jmoiron/sqlx v1.4.0 + github.com/ory/analytics-go/v5 v5.0.1 + github.com/ory/graceful v0.2.0 + github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 + github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e + github.com/ory/keto/gen/go v0.0.0-00010101000000-000000000000 + github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 + github.com/ory/x v0.0.0-00010101000000-000000000000 + github.com/pelletier/go-toml v1.9.5 + github.com/pkg/errors v0.9.1 + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.68.1 + github.com/rs/cors v1.11.1 + github.com/segmentio/objconv v1.0.1 + github.com/sirupsen/logrus v1.9.4 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.19.0 + github.com/tidwall/sjson v1.2.5 + github.com/urfave/negroni v1.0.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + go.uber.org/goleak v1.3.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 +) + +require ( + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect + buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 // indirect + buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect + buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda // indirect + buf.build/go/bufplugin v0.10.0 // indirect + buf.build/go/bufprivateusage v0.1.0 // indirect + buf.build/go/interrupt v1.1.0 // indirect + buf.build/go/protovalidate v1.2.0 // indirect + buf.build/go/protoyaml v0.7.0 // indirect + buf.build/go/spdx v0.2.0 // indirect + buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 // indirect + cel.dev/expr v0.25.2 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + code.dny.dev/ssrf v0.2.0 // indirect + connectrpc.com/otelconnect v0.9.0 // indirect + dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.4.2 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/sprig v2.15.0+incompatible // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/XSAM/otelsql v0.41.0 // indirect + github.com/a8m/envsubst v1.4.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/aokoli/goutils v1.0.1 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/avast/retry-go/v4 v4.6.1 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bradleyjkemp/cupaloy/v2 v2.8.0 // indirect + github.com/bufbuild/buf v1.70.0 // indirect + github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 // indirect + github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cli/browser v1.3.0 // indirect + github.com/cockroachdb/cockroach-go/v2 v2.4.3 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v29.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.7 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/elliotchance/orderedmap v1.7.1 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.23.0 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/inflect v0.21.0 // indirect + github.com/go-openapi/jsonpointer v0.21.2 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/loads v0.22.0 // indirect + github.com/go-openapi/runtime v0.28.0 // indirect + github.com/go-openapi/spec v0.21.0 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-swagger/go-swagger v0.31.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/fizz v1.14.4 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.10 // indirect + github.com/gobuffalo/nulls v0.4.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.22 // indirect + github.com/gobuffalo/plush/v5 v5.0.7 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/gofrs/flock v0.13.0 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/cel-go v0.28.1 // indirect + github.com/google/go-containerregistry v0.21.6 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/gorilla/handlers v1.5.2 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.10.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jaegertracing/jaeger-idl v0.9.0 // indirect + github.com/jdx/go-netrc v1.0.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josephburnett/jd v1.9.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.18.6 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/knadh/koanf/parsers/json v0.1.0 // indirect + github.com/knadh/koanf/parsers/toml v0.1.0 // indirect + github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect + github.com/knadh/koanf/providers/posflag v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.2.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.12.0 // indirect + github.com/magiconair/properties v1.8.9 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect + github.com/mattn/goveralls v0.0.12 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mikefarah/yq/v4 v4.45.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nyaruka/phonenumbers v1.6.5 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/ory/dockertest/v4 v4.0.0-beta.4 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/peterhellberg/link v1.2.0 // indirect + github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/pseudomuto/protoc-gen-doc v1.5.1 // indirect + github.com/pseudomuto/protokit v0.2.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.1 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/segmentio/backo-go v1.1.0 // indirect + github.com/segmentio/encoding v0.5.4 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.9.2 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tetratelabs/wazero v1.11.0 // indirect + github.com/tidwall/btree v1.8.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/toqueteos/webbrowser v1.2.0 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + go.lsp.dev/jsonrpc2 v0.10.0 // indirect + go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect + go.lsp.dev/protocol v0.12.0 // indirect + go.lsp.dev/uri v0.3.0 // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 // indirect + go.opentelemetry.io/contrib/propagators/b3 v1.44.0 // indirect + go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 // indirect + go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 // indirect + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.44.0 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.28.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.72.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.52.0 // indirect + mvdan.cc/xurls/v2 v2.6.0 // indirect + pluginrpc.com/pluginrpc v0.5.0 // indirect +) + +tool ( + connectrpc.com/connect/cmd/protoc-gen-connect-go + github.com/bufbuild/buf/cmd/buf + github.com/go-swagger/go-swagger/cmd/swagger + github.com/josephburnett/jd + github.com/mattn/goveralls + github.com/mikefarah/yq/v4 + github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc + golang.org/x/tools/cmd/goimports + google.golang.org/grpc/cmd/protoc-gen-go-grpc + google.golang.org/protobuf + google.golang.org/protobuf/cmd/protoc-gen-go +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d077093 --- /dev/null +++ b/go.sum @@ -0,0 +1,772 @@ +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 h1:zQ9C3e6FtwSZUFuKAQfpIKGFk5ZuRoGt5g35Bix55sI= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1/go.mod h1:1Znr6gmYBhbxWUPRrrVnSLXQsz8bvFVw1HHJq2bI3VQ= +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 h1:HwzzCRS4ZrEm1++rzSDxHnO0DOjiT1b8I/24e8a4exY= +buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1 h1:f8pa4iy1Bs+hQ16f3jg22rV/StDKIRj1rNNWX5rLwZ8= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.20.0-20260507063250-43b0c5a6cd08.1/go.mod h1:7MNigA51XJjPKrLnbcE61BmgW+pAp3mLW61gUqLBBRY= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1 h1:QK2GkcPxqh2oG5mTMAHejculun8nxto+p7mlgh8fPTM= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260507063250-43b0c5a6cd08.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts= +buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda h1:eysSyjrJtkxU1A/9+Kv+1Mwq9K6BYBw+STIOVsZ256Y= +buf.build/go/app v0.2.1-0.20260407195847-833f8f978cda/go.mod h1:V32mBaPWsfq6REAeZvvs/rQl7ZCl9Dn7eW1BBrmH0GQ= +buf.build/go/bufplugin v0.10.0 h1:vZBX0mq9as5UIBug8U+/DkGRaHNlM/HVOw59O8fvOIU= +buf.build/go/bufplugin v0.10.0/go.mod h1:ax7obVurKDH1I2nR4pFTS+TE6K3kZhTmwDCN2YgdV8I= +buf.build/go/bufprivateusage v0.1.0 h1:SzCoCcmzS3zyXHEXHeSQhGI7OTkgtljoknLzsUz9Gg4= +buf.build/go/bufprivateusage v0.1.0/go.mod h1:GlCCJ3VVF7EqqU0CoRmo1FzAwwaKymEWSr+ty69xU5w= +buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE= +buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM= +buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= +buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= +buf.build/go/protoyaml v0.7.0 h1:z4oVoFicbpPefhT7WAykxUdfp0yEQlhMQ2mCZOY5V38= +buf.build/go/protoyaml v0.7.0/go.mod h1:+a0cavd0uMvirb87xdu2ZMMmjlIQoiH/N2Ich5MGSQ0= +buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= +buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= +buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5 h1:njYKSWoLiq2i5O7y2bPPU2Yzp7iAU0Wk9KJ2OoAhNiU= +buf.build/go/standard v0.1.1-0.20260325175353-2b287e071df5/go.mod h1:DQmodNT9EHX94WzUaWiZK+/4EaFa/xZTc1gzfCxZVXU= +cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= +cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI= +code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +connectrpc.com/grpchealth v1.4.1-0.20260420170649-371aedad91aa h1:Yiwk49kx9uIK/4Spa4gyxrFpMpQSaFhIbciv6em3rAo= +connectrpc.com/grpchealth v1.4.1-0.20260420170649-371aedad91aa/go.mod h1:fC9WGwKmDruKCNh8wj2rThiaxxoiXxvsCVIu2Ex2voA= +connectrpc.com/grpcreflect v1.3.0 h1:Y4V+ACf8/vOb1XOc251Qun7jMB75gCUNw6llvB9csXc= +connectrpc.com/grpcreflect v1.3.0/go.mod h1:nfloOtCS8VUQOQ1+GTdFzVg2CJo4ZGaat8JIovCtDYs= +connectrpc.com/otelconnect v0.9.0 h1:NggB3pzRC3pukQWaYbRHJulxuXvmCKCKkQ9hbrHAWoA= +connectrpc.com/otelconnect v0.9.0/go.mod h1:AEkVLjCPXra+ObGFCOClcJkNjS7zPaQSqvO0lCyjfZc= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRDINkUyuvbb4b2Xm8w= +github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8= +github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM= +github.com/a8m/envsubst v1.4.2 h1:4yWIHXOLEJHQEFd4UjrWDrYeYlV7ncFWJOCBRLOZHQg= +github.com/a8m/envsubst v1.4.2/go.mod h1:MVUTQNGQ3tsjOOtKCNd+fl8RzhsXcDvvAEzkhGtlsbY= +github.com/alecthomas/assert/v2 v2.3.0 h1:mAsH2wmvjsuvyBvAmCtm7zFsBlb8mIHx5ySLVdDZXL0= +github.com/alecthomas/assert/v2 v2.3.0/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= +github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= +github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/bufbuild/buf v1.70.0 h1:rGL4TGoy8F1DbQa4BSlMOVBBR7lWblfnKxUdOxmeFns= +github.com/bufbuild/buf v1.70.0/go.mod h1:5gCCIpDmBzhiSJwqmxmbdN5aRZQYGmFSGnOBE7seP8c= +github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132 h1:f4T4k/41jHHhp2Otl6ZShDedr4wF9b+NdqIfLezx4R4= +github.com/bufbuild/protocompile v0.14.2-0.20260522222248-64e6ad034132/go.mod h1:jPUiZUFWc8E3Kc2Y4SRlGAdjde4amGkHY0BUACNS43E= +github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b h1:b7wvo9ZhjLzCp7tGbOUMvgtYTnd33zGSAmMxcdxMnhQ= +github.com/bufbuild/protoplugin v0.0.0-20260414125817-25d1d281b46b/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= +github.com/cenkalti/backoff/v3 v3.2.2 h1:cfUAAO3yvKMYKPrvhDuHSwQnhZNk/RMHKdZqKTxfm6M= +github.com/cenkalti/backoff/v3 v3.2.2/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= +github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= +github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= +github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= +github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v29.5.2+incompatible h1:ubykJ1Y8LmNRGJ2BuMQ0kHOt/RO1YzGNswqWMJgivuQ= +github.com/docker/cli v29.5.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA= +github.com/docker/docker-credential-helpers v0.9.7/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elliotchance/orderedmap v1.7.1 h1:8SR2DB391dw0HVI9572ElrY+KU0Q89OCXYwWZx7aAZc= +github.com/elliotchance/orderedmap v1.7.1/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= +github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= +github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= +github.com/go-swagger/go-swagger v0.31.0 h1:H8eOYQnY2u7vNKWDNykv2xJP3pBhRG/R+SOCAmKrLlc= +github.com/go-swagger/go-swagger v0.31.0/go.mod h1:WSigRRWEig8zV6t6Sm8Y+EmUjlzA/HoaZJ5edupq7po= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +github.com/gobuffalo/fizz v1.14.4 h1:8uume7joF6niTNWN582IQ2jhGTUoa9g1fiV/tIoGdBs= +github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM= +github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= +github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= +github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/helpers v0.6.10 h1:puKDCOrJ0EIq5ScnTRgKyvEZ05xQa+gwRGCpgoh6Ek8= +github.com/gobuffalo/helpers v0.6.10/go.mod h1:r52L6VSnByLJFOmURp1irvzgSakk7RodChi1YbGwk8I= +github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= +github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= +github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw= +github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8= +github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.22 h1:bPQr5PsiTg54UGMsfvnIAvFmUfxzD/ri+wbpu7PlmTM= +github.com/gobuffalo/plush/v4 v4.1.22/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/plush/v5 v5.0.7 h1:nI8sIt5tZAN2tCZHeaXkH7HAvxvvk3sJHG2TtrKeSHM= +github.com/gobuffalo/plush/v5 v5.0.7/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU= +github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= +github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= +github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= +github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= +github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.21.6 h1:T+yqQIlJXKrM98Om4DlW3GoWQAmhZuLMwoDOvVrtiUM= +github.com/google/go-containerregistry v0.21.6/go.mod h1:U7MMSBIJynke2MVQrQk19NP9k/uQsGz/h0amIFSHMbo= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaegertracing/jaeger-idl v0.9.0 h1:dI4olA7ArW3cjXwVbic/aYKDbdlfe7V+9wPQqAdzu8Y= +github.com/jaegertracing/jaeger-idl v0.9.0/go.mod h1:W+9vbcr2cVZyS6z/cbr540EOzSkKYml3hmaWEavxkB0= +github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= +github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josephburnett/jd v1.9.2 h1:ECJRRFXCCqbtidkAHckHGSZm/JIaAxS1gygHLF8MI5Y= +github.com/josephburnett/jd v1.9.2/go.mod h1:bImDr8QXpxMb3SD+w1cDRHp97xP6UwI88xUAuxwDQfM= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= +github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= +github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= +github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/laher/mergefs v0.1.1 h1:nV2bTS57vrmbMxeR6uvJpI8LyGl3QHj4bLBZO3aUV58= +github.com/laher/mergefs v0.1.1/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbCg= +github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mikefarah/yq/v4 v4.45.1 h1:EW+HjKEVa55pUYFJseEHEHdQ0+ulunY+q42zF3M7ZaQ= +github.com/mikefarah/yq/v4 v4.45.1/go.mod h1:djgN2vD749hpjVNGYTShr5Kmv5LYljhCG3lUTuEe3LM= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 h1:28i1IjGcx8AofiB4N3q5Yls55VEaitzuEPkFJEVgGkA= +github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nyaruka/phonenumbers v1.6.5 h1:aBCaUhfpRA7hU6fsXk+p7KF1aNx4nQlq9hGeo2qdFg8= +github.com/nyaruka/phonenumbers v1.6.5/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= +github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= +github.com/ory/dockertest/v4 v4.0.0-beta.4 h1:QcrNrobOP+5IjSDmS4//EuBtwiFuznQhi5xTe8oFSoM= +github.com/ory/dockertest/v4 v4.0.0-beta.4/go.mod h1:p9kfE14tzK8+WU4F9YbIZlzhCzQ2pH7H1KIfBKrF3DM= +github.com/ory/graceful v0.2.0 h1:5wqyCvKZG2+sorOA9rmQJm9aZP+RJdTrVHVDcvMOSuQ= +github.com/ory/graceful v0.2.0/go.mod h1:hg2iCy+LCWOXahBZ+NQa4dk8J2govyQD79rrqrgMyY8= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 h1:xSbAB0yH2CUPqf9tiei3EFo7xpqY5nsAGdcrwqH9o6o= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22/go.mod h1:by6EjQDF4bDC2OuWOBd9EBRrTjtl0+kxSzyvuizM0Iw= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e h1:4tUrC7x4YWRVMFp+c64KACNSGchW1zXo4l6Pa9/1hA8= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e/go.mod h1:XWLxVK4un/iuIcrw+6lCeanbF3NZwO5k6RdLeu/loQk= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 h1:LNreY5V2pXsUgiY/rVaU5uhIsOUHzZ+PP/NUgfqubr0= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6/go.mod h1:vd8H2inBRK+ZF+r5jDdx/fYPwqWrbVoAD4t7vAfXeA4= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= +github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM= +github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9 h1:arwj11zP0yJIxIRiDn22E0H8PxfF7TsTrc2wIPFIsf4= +github.com/protocolbuffers/protoscope v0.0.0-20221109213918-8e7a6aafa2c9/go.mod h1:SKZx6stCn03JN3BOWTwvVIO2ajMkb/zQdTceXYhKw/4= +github.com/pseudomuto/protoc-gen-doc v1.5.1 h1:Ah259kcrio7Ix1Rhb6u8FCaOkzf9qRBqXnvAufg061w= +github.com/pseudomuto/protoc-gen-doc v1.5.1/go.mod h1:XpMKYg6zkcpgfpCfQ8GcWBDRtRxOmMR5w7pz4Xo+dYM= +github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM= +github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= +github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= +github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 h1:0b8DF5kR0PhRoRXDiEEdzrgBc8UqVY4JWLkQJCRsLME= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= +github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= +github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= +github.com/segmentio/conf v1.2.0/go.mod h1:Y3B9O/PqqWqjyxyWWseyj/quPEtMu1zDp/kVbSWWaB0= +github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0= +github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/segmentio/go-snakecase v1.1.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= +github.com/segmentio/objconv v1.0.1 h1:QjfLzwriJj40JibCV3MGSEiAoXixbp4ybhwfTB8RXOM= +github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= +github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI= +go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw= +go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg= +go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ= +go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= +go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 h1:pV2nZ1iE87X9ym+crkD9k15zTLbN08+IC4C7sk9sQYM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1/go.mod h1:nvgrM8LaG2+5G7WxbtjEPiSkg87+d0/ltZZK70p7FVo= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0 h1:zv7PRYGLrQHkdeZj0c5SNAZOJcw55XgaTezUkNpwA+w= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0/go.mod h1:3+VZyCi6hFW+UuxFF+wSOvwsOwncfBpQfP7Qdb3JXKg= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= +go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a h1:+3jdDGGB8NGb1Zktc737jlt3/A5f6UlwSzmvqUuufxw= +golang.org/x/exp v0.0.0-20260508232706-74f9aab9d74a/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab h1:cY0oV1VnAqvaim8VsR8ZyEKAudzbRJMRGwD3W/L7yOw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260608224507-4308a22a1bab/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 h1:F29+wU6Ee6qgu9TddPgooOdaqsxTMunOoj8KA5yuS5A= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1/go.mod h1:5KF+wpkbTSbGcR9zteSqZV6fqFOWBl4Yde8En8MryZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 h1:6D+BvnJ/j6e222UW8s2qTSe3wGBtvo0MbVQG/c5k8RE= +gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473/go.mod h1:N1eN2tsCx0Ydtgjl4cqmbRCsY4/+z4cYDeqwZTk6zog= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= +modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= +modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= +modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI= +mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo= +pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o= diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..16797e2 --- /dev/null +++ b/install.sh @@ -0,0 +1,399 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2021-04-25T12:30:57Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echoerr "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echoerr "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null < !this.permits.view(ctx), + view: (ctx: Context): boolean => !this.permits.edit(ctx), + } + }`, + opts: []driver.TestRegistryOption{driver.WithConfig("limit.max_read_depth", 65535)}, + check: "Resource:doc#edit@User:alice", + restDepth: 65535, + want: cyclicError, + }, + { + name: "branching cycle aborts decisively", + opl: ` + class User implements Namespace {} + class R implements Namespace { + permits = { + a: (ctx: Context): boolean => this.permits.b(ctx) && this.permits.c(ctx), + b: (ctx: Context): boolean => this.permits.a(ctx) && this.permits.a(ctx), + c: (ctx: Context): boolean => this.permits.a(ctx) && this.permits.a(ctx), + } + }`, + check: "R:x#a@User:u", + restDepth: 5, + want: cyclicError, + }, + { + name: "self-loop is bounded by RestDepth", + opl: ` + class User implements Namespace {} + class Resource implements Namespace { + permits = { + edit: (ctx: Context): boolean => this.permits.edit(ctx), + } + }`, + check: "Resource:doc#edit@User:alice", + restDepth: 5, + want: depthLimited, + }, + { + name: "legit role chain resolves", + opl: ` + class User implements Namespace {} + class Doc implements Namespace { + related: { + l0: User[]; l1: User[]; l2: User[]; l3: User[]; + } + permits = { + p0: (ctx: Context): boolean => this.permits.p1(ctx) || this.related.l0.includes(ctx.subject), + p1: (ctx: Context): boolean => this.permits.p2(ctx) || this.related.l1.includes(ctx.subject), + p2: (ctx: Context): boolean => this.permits.p3(ctx) || this.related.l2.includes(ctx.subject), + p3: (ctx: Context): boolean => this.related.l3.includes(ctx.subject), + } + }`, + tuples: []string{"Doc:d#l3@User:alice"}, + check: "Doc:d#p0@User:alice", + restDepth: 5, + want: member, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + reg := newRegistryForOPL(t, tc.opl, tc.opts...) + if len(tc.tuples) > 0 { + testhelpers.MapAndInsertTuplesFromString(t, reg, tc.tuples) + } + rt := testhelpers.TupleFromString(t, tc.check) + + assertResult := func(t *testing.T, res check.Result) { + switch tc.want { + case cyclicError: + requireCyclicError(t, res.Err) + require.Equal(t, check.MembershipUnknown, res.Membership) + case member: + require.NoError(t, res.Err) + require.Equal(t, check.IsMember, res.Membership) + case depthLimited: + require.NoError(t, res.Err) + require.Equal(t, check.MembershipUnknown, res.Membership) + require.Equal(t, check.LimitationMaxDepthExceeded, res.Limitation) + } + } + + // Both executors wire the middleware separately. + t.Run("check engine", func(t *testing.T) { + assertResult(t, check.NewEngine(reg).CheckRelationTuple(t.Context(), rt, tc.restDepth)) + }) + t.Run("trace engine", func(t *testing.T) { + res, tree := trace.NewEngine(reg).CheckRelationTupleWithTrace(t.Context(), rt, tc.restDepth) + require.NotNil(t, tree) + assertResult(t, res) + }) + }) + } +} diff --git a/internal/check/definitions.go b/internal/check/definitions.go new file mode 100644 index 0000000..af9a78f --- /dev/null +++ b/internal/check/definitions.go @@ -0,0 +1,141 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "context" + + "github.com/ory/keto/internal/relationtuple" +) + +type ( + // Result holds the outcome of a single permission check. + // Limitation is non-empty only when Membership is MembershipUnknown. + Result struct { + Membership Membership + Err error + Limitation LimitationKind + } + + Membership int + + // LimitationKind identifies which traversal constraint was hit. A non-empty + // value on a Result means the membership answer is indeterminate. + LimitationKind string +) + +//go:generate stringer -type Membership +const ( + MembershipUnknown Membership = iota + IsMember + NotMember +) + +const ( + LimitationMaxDepthExceeded LimitationKind = "max_depth_exceeded" + LimitationMaxWidthExceeded LimitationKind = "max_width_exceeded" +) + +var ( + ResultUnknown = Result{Membership: MembershipUnknown} + ResultIsMember = Result{Membership: IsMember} + ResultNotMember = Result{Membership: NotMember} +) + +// CheckRequest carries all inputs for a single check evaluation step. +type CheckRequest struct { + Tuple *relationtuple.RelationTuple + RestDepth int +} + +func (r CheckRequest) WithDepth(d int) CheckRequest { + r.RestDepth = d + return r +} + +// PlannedStep pairs a Step with the CheckRequest it should run against. +type PlannedStep struct { + Step Step + Req CheckRequest +} + +// StepRunner orchestrates execution of multiple steps with short-circuit semantics. +// Typical usage: +// +// runner := ex.NewUnionRunner(ctx) +// for _, ps := range steps { +// if runner.Done() { break } +// runner.Add(ps) +// } +// return runner.Result() +type StepRunner interface { + // Add enqueues a step for execution. Returns early if Done() is true. + // No-op if the runner has already reached a decisive result or been cancelled. + Add(PlannedStep) + + // Done reports whether a decisive result was reached or context cancelled. + // Use to short-circuit the enqueue loop. + Done() bool + + // Ctx returns the runner's context, usable for shared state like visited tracking. + // The context is cancelled when a decisive result is reached (to signal siblings). + Ctx() context.Context + + // AddLimitation records that some branches were skipped due to given [LimitationKind] constraint. + AddLimitation(LimitationKind) + + // Result waits for all in-flight steps to complete and returns the aggregated result. + // Call it after the Add loop finishes (or breaks early on Done()). + Result() Result +} + +// Executor coordinates permission check execution with middleware support. +// It runs individual steps (Run), sequences steps with union/intersection logic +// (RunUnion, RunIntersection), and supports custom execution strategies via +// middleware. All Executor implementations are stateless; dependencies come +// from EngineDependencies. +// +// Typical usage: +// +// result := executor.Run(ctx, DirectStep{}, req) +// unionResult := executor.RunUnion(ctx, step1, step2) +// intersectionResult := executor.RunIntersection(ctx, step1, step2) +type Executor interface { + // Run executes a single step through the middleware chain. + Run(ctx context.Context, step Step, req CheckRequest) Result + + // RunUnion runs steps with OR semantics: returns IsMember on first match, + // short-circuits siblings, falls through to NotMember if none match. + RunUnion(ctx context.Context, steps ...PlannedStep) Result + + // RunIntersection runs steps with AND semantics: returns NotMember or Error + // on first failure, short-circuits siblings. + RunIntersection(ctx context.Context, steps ...PlannedStep) Result + + // NewUnionRunner creates a StepRunner for streaming union execution. + // It is useful when Steps are created dynamically and the full list is not known upfront. + NewUnionRunner(ctx context.Context) StepRunner + + Deps() EngineDependencies +} + +// Middleware intercepts a step execution within the pipeline. next continues +// down the remaining middlewares and ultimately calls step.Execute. +type Middleware func( + ctx context.Context, + step Step, + req CheckRequest, + next func(context.Context, Step, CheckRequest) Result, +) Result + +// Checker performs a full permission check for a single relation tuple. +// Implemented by Executor in the step package. +type ( + CheckerProvider interface { + Checker() Checker + } + Checker interface { + CheckRelationTuple(ctx context.Context, r *relationtuple.RelationTuple, restDepth int) Result + } +) diff --git a/internal/check/definitions_string.go b/internal/check/definitions_string.go new file mode 100644 index 0000000..1642548 --- /dev/null +++ b/internal/check/definitions_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type Membership"; DO NOT EDIT. + +package check + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[MembershipUnknown-0] + _ = x[IsMember-1] + _ = x[NotMember-2] +} + +const _Membership_name = "MembershipUnknownIsMemberNotMember" + +var _Membership_index = [...]uint8{0, 17, 25, 34} + +func (i Membership) String() string { + if i < 0 || i >= Membership(len(_Membership_index)-1) { + return "Membership(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Membership_name[_Membership_index[i]:_Membership_index[i+1]] +} diff --git a/internal/check/engine.go b/internal/check/engine.go new file mode 100644 index 0000000..91e2c88 --- /dev/null +++ b/internal/check/engine.go @@ -0,0 +1,127 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "context" + "net/http" + + "github.com/ory/herodot" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc/codes" + + "github.com/ory/keto/x/events" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +type ( + EngineProvider interface { + PermissionEngine() *Engine + } + + Engine struct { + dep EngineDependencies + } + + EngineDependencies interface { + relationtuple.ManagerProvider + relationtuple.MapperProvider + CheckerProvider + config.Provider + logrusx.Provider + otelx.Provider + } + + // Type alias for shorter signatures. + relationTuple = relationtuple.RelationTuple +) + +func errLimitationExceeded() *herodot.DefaultError { + return &herodot.DefaultError{ + StatusField: http.StatusText(http.StatusUnprocessableEntity), + ErrorField: "The check could not be completed", + CodeField: http.StatusUnprocessableEntity, + GRPCCodeField: codes.FailedPrecondition, + } +} + +// NewEngine creates an Engine that delegates checks to checker. +func NewEngine(d EngineDependencies) *Engine { + return &Engine{dep: d} +} + +// CheckIsMember checks if the relation tuple's subject has the relation on the +// object in the namespace either directly or indirectly and returns a boolean +// result. +func (e *Engine) CheckIsMember(ctx context.Context, r *relationTuple, restDepth int) (bool, error) { + result := e.CheckRelationTuple(ctx, r, restDepth) + if result.Err != nil { + return false, result.Err + } + if result.Membership == MembershipUnknown && result.Limitation != "" && e.dep.Config(ctx).StrictMode() { + return false, errLimitationExceeded().WithReason(string(result.Limitation)) + } + return result.Membership == IsMember, nil +} + +// CheckRelationTuple checks if the relation tuple's subject has the relation on +// the object in the namespace either directly or indirectly and returns a check +// result. +func (e *Engine) CheckRelationTuple(ctx context.Context, r *relationTuple, restDepth int) (res Result) { + ctx, span := e.dep.Tracer(ctx).Tracer().Start(ctx, "Engine.CheckRelationTuple") + defer otelx.End(span, &res.Err) + + txErr := e.dep.RelationTupleManager().WithLatestSnapshot(ctx, func(ctx context.Context) error { + res = e.dep.Checker().CheckRelationTuple(ctx, r, restDepth) + return res.Err + }) + if txErr != nil && res.Err == nil { + res.Membership = MembershipUnknown + res.Err = herodot.ErrInternalServerError().WithWrap(txErr) + + e.dep.Logger().WithError(txErr).WithField("tuple", r.String()).Error("could not open a consistent read snapshot for the check") + } + if res.Err == nil { + span.AddEvent(events.NewPermissionsChecked(ctx)) + } + return res +} + +// BatchCheck makes parallelized check requests for tuples. The check results +// are returned as a slice where the result index matches the input tuple index. +func (e *Engine) BatchCheck( + ctx context.Context, + tuples []*ketoapi.RelationTuple, + maxDepth int, +) ([]Result, error) { + eg := &errgroup.Group{} + eg.SetLimit(e.dep.Config(ctx).BatchCheckParallelizationLimit()) + + mapper := e.dep.ReadOnlyMapper() + results := make([]Result, len(tuples)) + for i, tuple := range tuples { + eg.Go(func() error { + internalTuple, err := mapper.FromTuple(ctx, tuple) + if err != nil { + results[i] = Result{ + Membership: MembershipUnknown, + Err: err, + } + } else { + results[i] = e.CheckRelationTuple(ctx, internalTuple[0], maxDepth) + } + return nil + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + return results, nil +} diff --git a/internal/check/engine_test.go b/internal/check/engine_test.go new file mode 100644 index 0000000..34581a6 --- /dev/null +++ b/internal/check/engine_test.go @@ -0,0 +1,845 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/step" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/internal/x/dbx" + "github.com/ory/keto/ketoapi" +) + +// ExpansionRecursive is a three-level group hierarchy where engineering branches +// into two sibling sub-groups. Safe to run with WithStrict(true). +// +// company ⊃ engineering ⊃ (frontend, backend) +// company ⊃ User:cto +// engineering ⊃ User:manager +// backend ⊃ User:Alice, Alice +// frontend ⊃ User:Bob, Bob +var ExpansionRecursive = testhelpers.Scenario{ + Name: "Expansion Recursive", + Opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: (User | SubjectSet)[] + } + } + `, + InputTuples: []string{ + "Group:company#members@Group:engineering#members", + + "Group:company#members@User:cto", + "Group:engineering#members@User:manager", + + "Group:engineering#members@Group:frontend#members", + "Group:engineering#members@Group:backend#members", + + "Group:backend#members@User:Alice", + "Group:frontend#members@User:Bob", + + "Group:backend#members@Alice", // SubjectID + "Group:frontend#members@Bob", // SubjectID + }, +} + +var ExpansionRecursiveNamespaceOnly = testhelpers.Scenario{ + Name: ExpansionRecursive.Name + " (namespaces only)", + Opl: `class User implements Namespace{} class Group implements Namespace{}`, + InputTuples: ExpansionRecursive.InputTuples, +} + +// SubjectSetExpansion3Hop combines cross-namespace and within-namespace SubjectSet expansion. +// +// File:team-report#viewers ⊃ File:team-report#editors ⊃ Group:finance#member → (User:Alice, User:Bob). +// File:personal-note#viewers ⊃ User:Alice, User:Bob +var SubjectSetExpansion3Hop = testhelpers.Scenario{ + Name: "SubjectSet Expansion 3 Hops", + Opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + member: User[] + } + } + class File implements Namespace{ + related: { + editors: (User | SubjectSet)[] + viewers: (User | SubjectSet)[] + } + } + `, + InputTuples: []string{ + "Group:finance#member@User:Alice", + "Group:finance#member@User:Bob", + + "File:team-report#editors@Group:finance#member", // group members are editors + "File:team-report#viewers@File:team-report#editors", // editors are also viewers + + "File:personal-note#viewers@User:Alice", + "File:personal-note#viewers@User:Bob", + }, +} + +var SubjectSetExpansion3HopNamespacesOnly = testhelpers.Scenario{ + Name: SubjectSetExpansion3Hop.Name + " (namespaces only)", + Opl: `class User implements Namespace{} class Group implements Namespace{} class File implements Namespace{}`, + InputTuples: SubjectSetExpansion3Hop.InputTuples, +} + +// CircularGraphNamespacesOnly covers a fully circular SubjectSet graph with no leaf subjects. +// +// A → B → C → A. +var CircularGraphNamespacesOnly = testhelpers.Scenario{ + Name: "Circular Graph (namespaces only)", + Opl: ` + class Folder implements Namespace{} + `, + InputTuples: []string{ + "Folder:a#parent@Folder:b#parent", + "Folder:b#parent@Folder:c#parent", + "Folder:c#parent@Folder:a#parent", + }, +} + +// FileWithNonConformingTuples covers a schema where a client incorrectly stores +// tuples directly under permit names (canEdit, canView) instead of the backing +// related fields (editors, viewers). +// +// In strict mode the engine ignores those tuples and resolves access only via +// OPL rewrites. In non-strict mode the direct tuples are visible alongside the +// rewrites. +var FileWithNonConformingTuples = testhelpers.Scenario{ + Name: "File With Non-Conforming Tuples", + Opl: ` + class User implements Namespace {} + class File implements Namespace { + related: { + editors: User[] + viewers: User[] + } + permits = { + canEdit: (ctx: Context) => this.related.editors.includes(ctx.subject), + canView: (ctx: Context) => this.permits.canEdit(ctx) || this.related.viewers.includes(ctx.subject), + } + } + `, + InputTuples: []string{ + "File:secret-doc#editors@User:Alice", + "File:secret-doc#viewers@User:Bob", + + // Bad client data: permit names used as direct relation targets. + "File:secret-doc#canEdit@User:Eve", + "File:secret-doc#canEdit@Eve", // subjectID + }, +} + +// NotionPage models a simplified Notion-like workspace with nested pages. +// +// canView requires direct or parent access. +// canEdit requires direct editor access AND workspace membership. +var NotionPage = testhelpers.Scenario{ + Name: "Notion Page", + Opl: ` + class User implements Namespace {} + class Workspace implements Namespace { + related: { + member: User[] + } + } + class Page implements Namespace { + related: { + workspace: Workspace[] + parent: Page[] + viewer: User[] + editor: User[] + } + permits = { + canView: (ctx: Context) => + this.related.viewer.includes(ctx.subject) || + this.related.editor.includes(ctx.subject) || + this.related.parent.traverse(p => p.permits.canView(ctx)), + canEdit: (ctx: Context) => + this.related.editor.includes(ctx.subject) && + this.related.workspace.traverse(w => w.related.member.includes(ctx.subject)), + } + } + `, + InputTuples: []string{ + "Workspace:acme#member@User:Alice", + "Workspace:acme#member@User:Bob", + + "Page:home#workspace@Workspace:acme", + "Page:home#viewer@User:Alice", + "Page:home#editor@User:Bob", + "Page:home#editor@User:Eve", // Eve is not workspace member + + "Page:intro#workspace@Workspace:acme", + "Page:intro#parent@Page:home", // intro inherits canView from home + "Page:intro#editor@User:Alice", + }, +} + +var testcases = []struct { + scenarios []testhelpers.Scenario + expectedMembers []string + expectedNonMembers []string +}{ + { + scenarios: []testhelpers.Scenario{ExpansionRecursive, ExpansionRecursiveNamespaceOnly}, + expectedMembers: []string{ + // direct: SubjectSet and SubjectID members + "Group:backend#members@User:Alice", "Group:backend#members@Alice", + "Group:frontend#members@User:Bob", "Group:frontend#members@Bob", + + // engineering: own direct member plus frontend and backend + "Group:engineering#members@User:manager", + "Group:engineering#members@User:Alice", "Group:engineering#members@Alice", + "Group:engineering#members@User:Bob", "Group:engineering#members@Bob", + + // company: own direct member plus everything from engineering + "Group:company#members@User:cto", + "Group:company#members@User:manager", + "Group:company#members@User:Alice", "Group:company#members@Alice", + "Group:company#members@User:Bob", "Group:company#members@Bob", + }, + expectedNonMembers: []string{ + // frontend and backend members do not cross. + "Group:frontend#members@User:Alice", + "Group:backend#members@User:Bob", + + // Membership flows upward only. + "Group:backend#members@User:manager", "Group:backend#members@User:cto", + "Group:frontend#members@User:manager", "Group:frontend#members@User:cto", + "Group:engineering#members@User:cto", + }, + }, + { + scenarios: []testhelpers.Scenario{SubjectSetExpansion3Hop, SubjectSetExpansion3HopNamespacesOnly}, + expectedMembers: []string{ + // direct + "Group:finance#member@User:Alice", "Group:finance#member@User:Bob", + + // via group expansion + "File:team-report#editors@User:Alice", "File:team-report#editors@User:Bob", + + // via editors are viewers + "File:team-report#viewers@User:Alice", "File:team-report#viewers@User:Bob", + + // direct viewers on personal note + "File:personal-note#viewers@User:Alice", "File:personal-note#viewers@User:Bob", + }, + expectedNonMembers: []string{ + // File:personal-note is missing subject-set relationship; + // Therefore editors are not viewers; + "File:personal-note#editors@User:Alice", + "File:personal-note#editors@User:Bob", + }, + }, + { + scenarios: []testhelpers.Scenario{CircularGraphNamespacesOnly}, + expectedMembers: []string{ + "Folder:a#parent@Folder:a#parent", + "Folder:a#parent@Folder:b#parent", + "Folder:a#parent@Folder:c#parent", + + "Folder:b#parent@Folder:a#parent", + "Folder:b#parent@Folder:b#parent", + "Folder:b#parent@Folder:c#parent", + + "Folder:c#parent@Folder:a#parent", + "Folder:c#parent@Folder:b#parent", + "Folder:c#parent@Folder:c#parent", + }, + }, + { + scenarios: []testhelpers.Scenario{{ + Name: "rejects transitive relation", + Opl: ` + class User implements Namespace{} + class Directory implements Namespace{} + class File implements Namespace{} + `, + InputTuples: []string{ + "File:report#parent@Directory:docs", + "Directory:docs#access@User:Alice", + }, + }}, + expectedNonMembers: []string{ + "File:report#access@User:Alice", + }, + }, + { + scenarios: []testhelpers.Scenario{FileWithNonConformingTuples.WithStrict(true)}, + expectedMembers: []string{ + "File:secret-doc#viewers@User:Bob", + "File:secret-doc#editors@User:Alice", + + // via OPL + "File:secret-doc#canView@User:Bob", + "File:secret-doc#canEdit@User:Alice", + "File:secret-doc#canView@User:Alice", + }, + expectedNonMembers: []string{ + "File:secret-doc#canEdit@User:Eve", + "File:secret-doc#canView@User:Eve", + + "File:secret-doc#canEdit@Eve", + "File:secret-doc#canView@Eve", + }, + }, + { + scenarios: []testhelpers.Scenario{NotionPage}, + expectedMembers: []string{ + // direct viewer/editor on home + "Page:home#canView@User:Alice", + "Page:home#canView@User:Bob", // editor also satisfies canView + "Page:home#canView@User:Eve", // stale editor tuple still satisfies canView (no workspace check) + // canEdit requires editor AND workspace member + "Page:home#canEdit@User:Bob", + // intro: direct editor + workspace member + "Page:intro#canEdit@User:Alice", + // intro canView via TTU: intro → parent → home → Alice/Bob/Eve + "Page:intro#canView@User:Alice", + "Page:intro#canView@User:Bob", + "Page:intro#canView@User:Eve", + }, + expectedNonMembers: []string{ + // Eve has editor tuple but is not a workspace member — AND fails + "Page:home#canEdit@User:Eve", + // Bob is editor of home, not intro — canEdit requires explicit editor tuple + "Page:intro#canEdit@User:Bob", + // Alice is only a viewer on home, not an editor + "Page:home#canEdit@User:Alice", + }, + }, + { + scenarios: []testhelpers.Scenario{FileWithNonConformingTuples}, + expectedMembers: []string{ + "File:secret-doc#viewers@User:Bob", + "File:secret-doc#editors@User:Alice", + + // via OPL + "File:secret-doc#canView@User:Bob", + "File:secret-doc#canEdit@User:Alice", + "File:secret-doc#canView@User:Alice", + + // canEdit is a direct tuple, visible in non-strict mode + "File:secret-doc#canEdit@User:Eve", + // canView is visible via OPL rewrite in non-strict mode + "File:secret-doc#canView@User:Eve", + }, + expectedNonMembers: []string{}, + }, +} + +func TestEngine(t *testing.T) { + t.Run(`membership tests`, func(t *testing.T) { + for _, tc := range testcases { + for _, scenario := range tc.scenarios { + scenario.Run(t, func(t *testing.T, reg driver.Registry) { + e := check.NewEngine(reg) + ctx := t.Context() + + for _, tuple := range tc.expectedMembers { + res, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, tuple), 5) + require.NoError(t, err) + assert.Truef(t, res, "expected %q to be a member", tuple) + } + + for _, tuple := range tc.expectedNonMembers { + res, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, tuple), 5) + require.NoError(t, err) + assert.Falsef(t, res, "expected %q to not be a member", tuple) + } + }) + } + } + }) + + t.Run("respects max-depth", func(t *testing.T) { + ExpansionRecursive.Run(t, func(t *testing.T, reg driver.Registry) { + e := check.NewEngine(reg) + ctx := t.Context() + + // company → engineering → backend → User:Alice requires exactly 2 hops. + tuple := testhelpers.TupleFromString(t, "Group:company#members@User:Alice") + + assert.Equal(t, 5, reg.Config(ctx).MaxReadDepth()) + + res, err := e.CheckIsMember(ctx, tuple, 1) + require.NoError(t, err) + assert.False(t, res, "depth=1 is not enough for a 2-hop chain") + + res, err = e.CheckIsMember(ctx, tuple, 2) + require.NoError(t, err) + assert.True(t, res, "depth=2 is enough for a 2-hop chain") + + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadDepth, 1)) + res, err = e.CheckIsMember(ctx, tuple, 5) + require.NoError(t, err) + assert.False(t, res, "global max-depth=1 overrides request depth=5") + + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadDepth, 3)) + res, err = e.CheckIsMember(ctx, tuple, 0) + require.NoError(t, err) + assert.True(t, res, "global max-depth=3 is enough") + }) + }) + + t.Run("batch check", func(t *testing.T) { + SubjectSetExpansion3Hop.Run(t, func(t *testing.T, reg driver.Registry) { + e := check.NewEngine(reg) + ctx := t.Context() + + targetTuples := []*ketoapi.RelationTuple{ + testhelpers.APITupleFromString(t, "Group:finance#member@User:Alice"), // direct + testhelpers.APITupleFromString(t, "File:team-report#editors@User:Alice"), // 1-hop + testhelpers.APITupleFromString(t, "File:team-report#viewers@User:Alice"), // 2-hop + testhelpers.APITupleFromString(t, "NonExistent:x#rel@User:Alice"), // non-existent namespace + testhelpers.APITupleFromString(t, "Group:finance#member@User:Eve"), // unknown subject + testhelpers.APITupleFromString(t, "File:team-report#viewers@File:team-report#editors"), // via subject set + } + + // At depth=1, the 2-hop viewers chain is not resolved. + results, err := e.BatchCheck(ctx, targetTuples, 1) + require.NoError(t, err) + + require.Equal(t, check.IsMember, results[0].Membership) + require.Equal(t, check.IsMember, results[1].Membership) + require.Equal(t, check.MembershipUnknown, results[2].Membership) + require.Equal(t, check.LimitationMaxDepthExceeded, results[2].Limitation) + require.Equal(t, check.MembershipUnknown, results[3].Membership) + require.EqualError(t, results[3].Err, herodot.ErrNotFound().Error()) + require.Equal(t, check.MembershipUnknown, results[4].Membership) + require.Equal(t, check.LimitationMaxDepthExceeded, results[4].Limitation) + require.Equal(t, check.IsMember, results[5].Membership) + + // At depth=2, the 2-hop viewers chain resolves + results, err = e.BatchCheck(ctx, targetTuples, 3) + require.NoError(t, err) + require.Equal(t, check.IsMember, results[2].Membership) + }) + }) +} + +// widthLimitScenario fans out Resource:doc#readers into three groups. +// With width=1 only Group:alpha is explored; the rest are truncated. +var widthLimitScenario = testhelpers.Scenario{ + Name: "Width Limit", + OrderedShardID: true, + Opl: ` + class User implements Namespace {} + class Group implements Namespace { + related: { + member: (User | SubjectSet)[] + } + } + class Resource implements Namespace { + related: { + readers: (User | SubjectSet)[] + } + } + `, + // Resource:doc#readers + // ├── Group:alpha#member ──► Group:alpha-sub#member ──► User:Alice (explored first at width=1) + // ├── Group:beta#member ──► Group:beta-sub#member ──► User:Bob (truncated at width=1) + // └── Group:gamma#member ──► Group:gamma-sub#member (truncated at width=1) + InputTuples: []string{ + // Insertion order determines DB return order for expand + "Resource:doc#readers@Group:alpha#member", + "Resource:doc#readers@Group:beta#member", + "Resource:doc#readers@Group:gamma#member", + + "Group:alpha#member@Group:alpha-sub#member", + "Group:beta#member@Group:beta-sub#member", + "Group:gamma#member@Group:gamma-sub#member", + + "Group:alpha-sub#member@User:Alice", + "Group:beta-sub#member@User:Bob", + // Eve is in none of the groups. + }, +} + +func TestWidthLimit(t *testing.T) { + type tc struct { + name string + subject string + width int // 0 means default 100 + expectedMembership check.Membership + expectedLimitation check.LimitationKind + expectedIsMember bool + } + + tests := []tc{ + { + name: "no limit: Alice is IsMember", + subject: "User:Alice", + expectedMembership: check.IsMember, + expectedIsMember: true, + }, + { + name: "no limit: Eve is NotMember", + subject: "User:Eve", + expectedMembership: check.NotMember, + expectedIsMember: false, + }, + { + name: "width=1: Eve is indeterminate", + subject: "User:Eve", + width: 1, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxWidthExceeded, + expectedIsMember: false, + }, + { + // Bob is only in beta-sub, which is always truncated at width=1. + name: "width=1: member only in truncated branch is indeterminate", + subject: "User:Bob", + width: 1, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxWidthExceeded, + expectedIsMember: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + widthLimitScenario.Run(t, func(t *testing.T, reg driver.Registry) { + ctx := t.Context() + e := check.NewEngine(reg) + if tc.width > 0 { + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadWidth, tc.width)) + } + + res := e.CheckRelationTuple(ctx, testhelpers.TupleFromString(t, "Resource:doc#readers@"+tc.subject), 10) + require.NoError(t, res.Err) + assert.Equal(t, tc.expectedMembership, res.Membership) + assert.Equal(t, tc.expectedLimitation, res.Limitation) + + isMember, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, "Resource:doc#readers@"+tc.subject), 10) + require.NoError(t, err) + assert.Equal(t, tc.expectedIsMember, isMember) + }) + + // in strict mode, expect Error when limitation is hit + widthLimitScenario.WithStrict(true).Run(t, func(t *testing.T, reg driver.Registry) { + ctx := t.Context() + e := check.NewEngine(reg) + if tc.width > 0 { + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadWidth, tc.width)) + } + + res := e.CheckRelationTuple(ctx, testhelpers.TupleFromString(t, "Resource:doc#readers@"+tc.subject), 10) + require.NoError(t, res.Err) + assert.Equal(t, tc.expectedMembership, res.Membership) + assert.Equal(t, tc.expectedLimitation, res.Limitation) + + if tc.expectedLimitation != "" { + _, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, "Resource:doc#readers@"+tc.subject), 10) + require.Error(t, err) + var he *herodot.DefaultError + require.ErrorAs(t, err, &he) + assert.Contains(t, he.Reason(), string(tc.expectedLimitation)) + } else { + isMember, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, "Resource:doc#readers@"+tc.subject), 10) + require.NoError(t, err) + assert.Equal(t, tc.expectedIsMember, isMember) + } + }) + }) + } +} + +// Alice is in both deep and short chains. +// Depth of 3 is needed to reach Alice via shortRel. +// Depth of 5 is needed to reach Alice via deepRel. +var depthLimitScenario = testhelpers.Scenario{ + Name: "Depth Limit", + Opl: ` + class User implements Namespace {} + class Group implements Namespace { + related: { + members: (User | SubjectSet)[] + } + } + class Resource implements Namespace { + related: { + deepRel: (User | SubjectSet)[] + shortRel: (User | SubjectSet)[] + } + permits = { + union: (ctx:Context) => + this.related.deepRel.includes(ctx.subject) + || this.related.shortRel.includes(ctx.subject), + intersection: (ctx:Context) => + this.related.deepRel.includes(ctx.subject) + && this.related.shortRel.includes(ctx.subject), + shortButNotDeep: (ctx:Context) => this.related.shortRel.includes(ctx.subject) + && !this.related.deepRel.includes(ctx.subject), + + // notEither simulates a client that authorizes users when Keto returns NotMember + notEither: (ctx:Context) => !this.permits.union(ctx), + } + } + `, + InputTuples: []string{ + "Resource:doc#deepRel@Group:deep#members", + "Resource:doc#shortRel@Group:short#members", + + "Group:deep#members@Group:deep1#members", + "Group:deep1#members@Group:deep2#members", + "Group:deep2#members@Group:deep3#members", + "Group:deep3#members@Group:deep4#members", + "Group:deep4#members@User:Alice", + + "Group:short#members@Group:short1#members", + "Group:short1#members@Group:short2#members", + "Group:short2#members@User:Alice", + "Group:short2#members@User:Charlie", + }, +} + +func TestDepthLimit(t *testing.T) { + type tc struct { + name string + tuple string + depth int + expectedMembership check.Membership + expectedLimitation check.LimitationKind + expectedIsMember bool + } + + tests := []tc{ + { + name: "union: too shallow depth for both branches yields NotMember", + tuple: "Resource:doc#union@User:Alice", + depth: 2, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "union: enough depth for 1 Member branch yields Member", + tuple: "Resource:doc#union@User:Alice", + depth: 4, + expectedMembership: check.IsMember, + expectedIsMember: true, + }, + { + name: "union: enough depth for 1 NotMember branch yields Unknown", + tuple: "Resource:doc#union@User:Bob", // Bob is in no branch + depth: 4, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "intersection: too shallow depth for both branches yields Unknown", + tuple: "Resource:doc#intersection@User:Alice", + depth: 2, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "intersection: enough depth for only 1 branch yields Unknown", + tuple: "Resource:doc#intersection@User:Alice", + depth: 4, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "intersection: enough depth for both branches yields Member", + tuple: "Resource:doc#intersection@User:Alice", + depth: 5, + expectedMembership: check.IsMember, + expectedIsMember: true, + }, + { + name: "shortButNotDeep: too shallow depth for both branches yields Unknown", + tuple: "Resource:doc#shortButNotDeep@User:Alice", + depth: 2, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "shortButNotDeep: enough depth for only 1 branch yields Unknown", + tuple: "Resource:doc#shortButNotDeep@User:Alice", + depth: 4, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + { + name: "shortButNotDeep: user only in shortRel yields Member at sufficient depth", + tuple: "Resource:doc#shortButNotDeep@User:Charlie", + depth: 7, + expectedMembership: check.IsMember, + expectedIsMember: true, + }, + { + name: "notEither: not enough depth for any branch yields Unknown", + tuple: "Resource:doc#notEither@User:Alice", + depth: 2, + expectedMembership: check.MembershipUnknown, + expectedLimitation: check.LimitationMaxDepthExceeded, + expectedIsMember: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + depthLimitScenario.Run(t, func(t *testing.T, reg driver.Registry) { + ctx := t.Context() + e := check.NewEngine(reg) + if tc.depth > 0 { + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadDepth, tc.depth)) + } + + res := e.CheckRelationTuple(ctx, testhelpers.TupleFromString(t, tc.tuple), 10) + require.NoError(t, res.Err) + assert.Equal(t, tc.expectedMembership, res.Membership) + assert.Equal(t, tc.expectedLimitation, res.Limitation) + + isMember, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, tc.tuple), 10) + require.NoError(t, err) + assert.Equal(t, tc.expectedIsMember, isMember) + }) + + // in strict mode, expect Error when limitation is hit + depthLimitScenario.WithStrict(true).Run(t, func(t *testing.T, reg driver.Registry) { + ctx := t.Context() + e := check.NewEngine(reg) + if tc.depth > 0 { + require.NoError(t, reg.Config(ctx).Set(config.KeyLimitMaxReadDepth, tc.depth)) + } + + res := e.CheckRelationTuple(ctx, testhelpers.TupleFromString(t, tc.tuple), 10) + require.NoError(t, res.Err) + assert.Equal(t, tc.expectedMembership, res.Membership) + assert.Equal(t, tc.expectedLimitation, res.Limitation) + + if tc.expectedLimitation != "" { + _, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, tc.tuple), 10) + require.Error(t, err) + var he *herodot.DefaultError + require.ErrorAs(t, err, &he) + assert.Contains(t, he.Reason(), string(tc.expectedLimitation)) + } else { + isMember, err := e.CheckIsMember(ctx, testhelpers.TupleFromString(t, tc.tuple), 10) + require.NoError(t, err) + assert.Equal(t, tc.expectedIsMember, isMember) + } + }) + }) + } +} + +// TestCheckSnapshotIsolation verifies that a concurrent write committed between +// two SQL queries within the same check does not affect the result. +func TestCheckSnapshotIsolation(t *testing.T) { + t.Parallel() + + const opl = ` + class User implements Namespace {} + class Folder implements Namespace { + related: { + members: User[] + } + permits = { + isMember: (ctx: Context) => this.related.members.includes(ctx.subject), + } + } + class File implements Namespace { + related: { + parent: Folder[] + } + permits = { + view: (ctx: Context) => this.related.parent.traverse(p => p.permits.isMember(ctx)), + } + } + ` + inputTuples := []string{ + "File:readme#parent@Folder:docs", + "Folder:docs#members@User:u1", // this tuple will be deleted during check execution + } + + var snapshotDSNs []*dbx.DsnT + for _, dsn := range dbx.GetDSNs(t) { + switch dsn.Name { + case "cockroach", "mysql", "postgres": + snapshotDSNs = append(snapshotDSNs, dsn) + } + } + if len(snapshotDSNs) == 0 { + // SQLite is excluded. pop serialises all SQLite transactions with a mutex, + // so the middleware's concurrent delete gets deadlocked against the outer check + // transaction + t.Skip("sqlite is excluded from snapshot isolation test; run without -short to start CockroachDB/MySQL/PostgreSQL") + } + + for _, dsn := range snapshotDSNs { + t.Run("dsn="+dsn.Name, func(t *testing.T) { + t.Parallel() + + reg := driver.NewTestRegistry(t, dsn, + driver.WithOPL(opl), + driver.WithMapperNamespace(testhelpers.CustomMapperNamespace), + driver.WithConfig(config.KeyFeatureFlagStrictMode, true), + ) + testhelpers.MapAndInsertTuplesFromString(t, reg, inputTuples) + + ctx := t.Context() + + // The delete must happen after the first SQL query in the transaction so + // the snapshot is already established in PostgreSQL/MySQL REPEATABLE READ + // (which snapshots at first statement, not at BEGIN). + var once sync.Once + middleware := func(mCtx context.Context, s check.Step, req check.CheckRequest, next func(context.Context, check.Step, check.CheckRequest) check.Result) check.Result { + if s.Kind() == check.StepIsAllowed && req.Tuple.Namespace == "Folder" && req.Tuple.Relation == "isMember" { + once.Do(func() { + assert.NoError(t, reg.RelationTupleManager().DeleteRelationTuples(t.Context(), + testhelpers.TupleFromString(t, "Folder:docs#members@User:u1"), + )) + }) + } + return next(mCtx, s, req) + } + + engine := check.NewEngine(&checkerOverrideRegistry{Registry: reg, mw: middleware}) + + result := engine.CheckRelationTuple(ctx, testhelpers.TupleFromString(t, "File:readme#view@User:u1"), 5) + require.NoError(t, result.Err) + assert.Equal(t, check.IsMember, result.Membership, + "check must reflect the database state at the start of the evaluation") + }) + } +} + +// checkerOverrideRegistry wraps a Registry and replaces its Checker. +// mw can be used to inject a middleware to the Executor. +type checkerOverrideRegistry struct { + driver.Registry + mw check.Middleware +} + +func (r *checkerOverrideRegistry) Checker() check.Checker { + return step.NewExecutor(r.Registry, r.mw) +} diff --git a/internal/check/handler.go b/internal/check/handler.go new file mode 100644 index 0000000..199eda3 --- /dev/null +++ b/internal/check/handler.go @@ -0,0 +1,526 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "connectrpc.com/connect" + "github.com/pkg/errors" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/herodot" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x" + "github.com/ory/keto/ketoapi" +) + +type ( + handlerDependencies interface { + EngineProvider + relationtuple.ManagerProvider + relationtuple.MapperProvider + logrusx.Provider + httpx.WriterProvider + config.Provider + x.HandlerOptionsProvider + } + Handler struct { + relationtuplesconnect.UnimplementedCheckServiceHandler + d handlerDependencies + } +) + +var ( + _ relationtuplesconnect.CheckServiceHandler = (*Handler)(nil) + _ *checkPermission = nil +) + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } + +const ( + RouteBase = "/relation-tuples/check" + OpenAPIRouteBase = RouteBase + "/openapi" + BatchRoute = "/relation-tuples/batch/check" +) + +func (h *Handler) RegisterReadRoutes(r *httprouterx.RouterPublic) { + r.GET(RouteBase, h.getCheckMirrorStatus) + r.GET(OpenAPIRouteBase, h.getCheckNoStatus) + r.POST(RouteBase, h.postCheckMirrorStatus) + r.POST(OpenAPIRouteBase, h.postCheckNoStatus) + r.POST(BatchRoute, h.batchCheck) + + checkServiceMethods := rts.File_ory_keto_relation_tuples_v1alpha2_check_service_proto.Services().ByName("CheckService").Methods() + + checkHandler := connect.NewUnaryHandler( + relationtuplesconnect.CheckServiceCheckProcedure, + h.Check, + connect.WithSchema(checkServiceMethods.ByName("Check")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.CheckServiceCheckProcedure, checkHandler) + + batchCheckHandler := connect.NewUnaryHandler( + relationtuplesconnect.CheckServiceBatchCheckProcedure, + h.BatchCheck, + connect.WithSchema(checkServiceMethods.ByName("BatchCheck")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.CheckServiceBatchCheckProcedure, batchCheckHandler) +} + +func (h *Handler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{ + rts.File_ory_keto_relation_tuples_v1alpha2_check_service_proto, + rts.File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto, + } +} + +// Check Permission Result +// +// The content of the allowed field is mirrored in the HTTP status code. +// +// swagger:model checkPermissionResult +type CheckPermissionResult struct { + // whether the relation tuple is allowed + // + // required: true + Allowed bool `json:"allowed"` +} + +// Check Permission Result With Error +// +// swagger:model checkPermissionResultWithError +type CheckPermissionResultWithError struct { + // whether the relation tuple is allowed + // + // required: true + Allowed bool `json:"allowed"` + // any error generated while checking the relation tuple + // + // required: false + Error string `json:"error,omitempty"` +} + +// Check Permission Request Parameters +// +// swagger:parameters checkPermission +type checkPermission struct { + // in: query + MaxDepth int `json:"max-depth"` +} + +// swagger:route GET /relation-tuples/check/openapi permission checkPermission +// +// # Check a permission +// +// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: checkPermissionResult +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-public-high +func (h *Handler) getCheckNoStatus(w http.ResponseWriter, r *http.Request) { + allowed, err := h.getCheck(r.Context(), r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed}) +} + +// Check Permission Or Error Request Parameters +// +// swagger:parameters checkPermissionOrError +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type checkPermissionOrError struct { + // in: query + MaxDepth int `json:"max-depth"` +} + +// swagger:route GET /relation-tuples/check permission checkPermissionOrError +// +// # Check a permission +// +// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: checkPermissionResult +// 400: errorGeneric +// 403: checkPermissionResult +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-public-high +func (h *Handler) getCheckMirrorStatus(w http.ResponseWriter, r *http.Request) { + allowed, err := h.getCheck(r.Context(), r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if allowed { + h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed}) + return + } + + h.d.Writer().WriteCode(w, r, http.StatusForbidden, &CheckPermissionResult{Allowed: allowed}) +} + +func (h *Handler) getCheck(ctx context.Context, q url.Values) (bool, error) { + maxDepth, err := x.GetMaxDepthFromQuery(q) + if err != nil { + return false, err + } + + tuple, err := (&ketoapi.RelationTuple{}).FromURLQuery(q) + if err != nil { + return false, err + } + + it, err := h.d.ReadOnlyMapper().FromTuple(ctx, tuple) + // herodot.ErrNotFound occurs when the namespace is unknown + if errors.Is(err, herodot.ErrNotFound()) { + return false, nil + } else if err != nil { + return false, err + } + + return h.d.PermissionEngine().CheckIsMember(ctx, it[0], maxDepth) +} + +// Check Permission using Post Request Parameters +// +// swagger:parameters postCheckPermission +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type postCheckPermission struct { + // in: query + MaxDepth int `json:"max-depth"` + + // in: body + Payload postCheckPermissionBody +} + +// Check Permission using Post Request Body +// +// swagger:model postCheckPermissionBody +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type postCheckPermissionBody struct { + ketoapi.RelationQuery +} + +// swagger:route POST /relation-tuples/check/openapi permission postCheckPermission +// +// # Check a permission +// +// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: checkPermissionResult +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-public-high +func (h *Handler) postCheckNoStatus(w http.ResponseWriter, r *http.Request) { + allowed, err := h.postCheck(r.Context(), r.Body, r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed}) +} + +// Post Check Permission Or Error Request Parameters +// +// swagger:parameters postCheckPermissionOrError +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type postCheckPermissionOrError struct { + // in: query + MaxDepth int `json:"max-depth"` + + // in: body + Body postCheckPermissionOrErrorBody +} + +// Post Check Permission Or Error Body +// +// swagger:model postCheckPermissionOrErrorBody +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type postCheckPermissionOrErrorBody struct { + ketoapi.RelationQuery +} + +// swagger:route POST /relation-tuples/check permission postCheckPermissionOrError +// +// # Check a permission +// +// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: checkPermissionResult +// 400: errorGeneric +// 403: checkPermissionResult +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-public-high +func (h *Handler) postCheckMirrorStatus(w http.ResponseWriter, r *http.Request) { + allowed, err := h.postCheck(r.Context(), r.Body, r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + if allowed { + h.d.Writer().Write(w, r, &CheckPermissionResult{Allowed: allowed}) + return + } + + h.d.Writer().WriteCode(w, r, http.StatusForbidden, &CheckPermissionResult{Allowed: allowed}) +} + +func (h *Handler) postCheck(ctx context.Context, body io.Reader, query url.Values) (bool, error) { + maxDepth, err := x.GetMaxDepthFromQuery(query) + if err != nil { + return false, err + } + + var tuple ketoapi.RelationTuple + if err := json.NewDecoder(body).Decode(&tuple); err != nil { + return false, errors.WithStack(herodot.ErrBadRequest().WithErrorf("could not unmarshal json: %s", err.Error())) + } + t, err := h.d.ReadOnlyMapper().FromTuple(ctx, &tuple) + // herodot.ErrNotFound occurs when the namespace is unknown + if errors.Is(err, herodot.ErrNotFound()) { + return false, nil + } else if err != nil { + return false, err + } + + return h.d.PermissionEngine().CheckIsMember(ctx, t[0], maxDepth) +} + +func (h *Handler) Check(ctx context.Context, req *connect.Request[rts.CheckRequest]) (*connect.Response[rts.CheckResponse], error) { + var src ketoapi.TupleData + if req.Msg.Tuple != nil { + src = req.Msg.Tuple + } else { + src = req.Msg + } + + tuple, err := (&ketoapi.RelationTuple{}).FromDataProvider(src) + if err != nil { + return nil, err + } + + internalTuple, err := h.d.ReadOnlyMapper().FromTuple(ctx, tuple) + if err != nil { + return nil, err + } + allowed, err := h.d.PermissionEngine().CheckIsMember(ctx, internalTuple[0], int(req.Msg.MaxDepth)) + // TODO add content change handling + if err != nil { + return nil, err + } + + return connect.NewResponse(&rts.CheckResponse{ //nolint:gosec // Snaptoken is not a credential, just a placeholder value. + Allowed: allowed, + Snaptoken: "not yet implemented", + }), nil +} + +// Batch Check Permission Request Parameters +// +// swagger:parameters batchCheckPermission +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type batchCheckPermission struct { + // in: query + MaxDepth int `json:"max-depth"` + + // in: body + Body batchCheckPermissionBody +} + +// Batch Check Permission Body +// +// swagger:model batchCheckPermissionBody +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type batchCheckPermissionBody struct { + Tuples []*ketoapi.RelationTuple `json:"tuples"` +} + +// Batch Check Permission Result +// +// swagger:model batchCheckPermissionResult +type BatchCheckPermissionResult struct { + // An array of check results. The order aligns with the input order. + // + // required: true + Results []*CheckPermissionResultWithError `json:"results"` +} + +// swagger:route POST /relation-tuples/batch/check permission batchCheckPermission +// +// # Batch check permissions +// +// To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: batchCheckPermissionResult +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-public-high +func (h *Handler) batchCheck(w http.ResponseWriter, r *http.Request) { + results, err := h.doBatchCheck(r.Context(), r.Body, r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, &BatchCheckPermissionResult{Results: results}) +} + +// doBatchCheck is the HTTP entry point for checking batches of tuples +func (h *Handler) doBatchCheck(ctx context.Context, body io.Reader, query url.Values) ([]*CheckPermissionResultWithError, error) { + maxDepth, err := x.GetMaxDepthFromQuery(query) + if err != nil { + return nil, err + } + var request batchCheckPermissionBody + if err := json.NewDecoder(body).Decode(&request); err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithErrorf("could not unmarshal json: %s", err.Error())) + } + + if len(request.Tuples) > h.d.Config(ctx).BatchCheckMaxBatchSize() { + return nil, herodot.ErrBadRequest().WithErrorf("batch exceeds max size of %v", + h.d.Config(ctx).BatchCheckMaxBatchSize()) + } + + results, err := h.d.PermissionEngine().BatchCheck(ctx, request.Tuples, maxDepth) + if err != nil { + return nil, err + } + + strict := h.d.Config(ctx).StrictMode() + responses := make([]*CheckPermissionResultWithError, len(request.Tuples)) + for i, result := range results { + responses[i] = &CheckPermissionResultWithError{ + Allowed: result.Membership == IsMember, + Error: batchResultErrMsg(result, strict), + } + } + + return responses, nil +} + +// BatchCheck is the gRPC entry point for checking batches of tuples +func (h *Handler) BatchCheck(ctx context.Context, req *connect.Request[rts.BatchCheckRequest]) (*connect.Response[rts.BatchCheckResponse], error) { + if len(req.Msg.Tuples) > h.d.Config(ctx).BatchCheckMaxBatchSize() { + return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf( + "batch exceeds max size of %v", h.d.Config(ctx).BatchCheckMaxBatchSize())) + } + + ketoTuples := make([]*ketoapi.RelationTuple, len(req.Msg.Tuples)) + for i, tuple := range req.Msg.Tuples { + ketoTuples[i] = (&ketoapi.RelationTuple{}).FromProto(tuple) + } + + results, err := h.d.PermissionEngine().BatchCheck(ctx, ketoTuples, int(req.Msg.MaxDepth)) + if err != nil { + return nil, err + } + + strict := h.d.Config(ctx).StrictMode() + responses := make([]*rts.CheckResponseWithError, len(results)) + for i, result := range results { + responses[i] = &rts.CheckResponseWithError{ //nolint:gosec // Snaptoken is not a credential, just a placeholder value. + Allowed: result.Membership == IsMember, + Error: batchResultErrMsg(result, strict), + Snaptoken: "not yet implemented", + } + } + + return connect.NewResponse(&rts.BatchCheckResponse{ + Results: responses, + }), nil +} + +// batchResultErrMsg returns an error string for a single batch check result. +// In strict mode a limitation is surfaced so callers can distinguish +// "definitely not a member" from "could not determine membership". +func batchResultErrMsg(r Result, strict bool) string { + if r.Err != nil { + return r.Err.Error() + } + if r.Membership == MembershipUnknown && r.Limitation != "" && strict { + return string(r.Limitation) + } + return "" +} diff --git a/internal/check/handler_test.go b/internal/check/handler_test.go new file mode 100644 index 0000000..675aa5a --- /dev/null +++ b/internal/check/handler_test.go @@ -0,0 +1,415 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/ory/x/httprouterx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/status" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + client "github.com/ory/keto/internal/httpclient" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/ketoapi" +) + +func readBody(t *testing.T, resp *http.Response) []byte { + t.Helper() + defer func() { assert.NoError(t, resp.Body.Close()) }() + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + return body +} + +func assertAllowed(t *testing.T, resp *http.Response) { + t.Helper() + body := readBody(t, resp) + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + allowed := gjson.GetBytes(body, "allowed") + require.True(t, allowed.Exists(), "response missing 'allowed' field: %s", body) + require.True(t, allowed.Bool()) +} + +type responseAssertion func(t *testing.T, resp *http.Response) + +func baseAssertDenied(t *testing.T, resp *http.Response) { + t.Helper() + body := readBody(t, resp) + require.Equal(t, http.StatusForbidden, resp.StatusCode, "%s", body) + allowed := gjson.GetBytes(body, "allowed") + require.True(t, allowed.Exists(), "response missing 'allowed' field: %s", body) + require.False(t, allowed.Bool()) +} + +// For OpenAPI clients, we want to always return a 200 status code even if the +// check returned "denied" to not cause exceptions etc. in the generated clients. +func openAPIAssertDenied(t *testing.T, resp *http.Response) { + t.Helper() + body := readBody(t, resp) + require.Equal(t, http.StatusOK, resp.StatusCode, "%s", body) + allowed := gjson.GetBytes(body, "allowed") + require.True(t, allowed.Exists(), "response missing 'allowed' field: %s", body) + require.False(t, allowed.Bool()) +} + +func newTestGRPCCheckClient(t *testing.T, h *check.Handler) rts.CheckServiceClient { + t.Helper() + router := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(router) + ts := httptest.NewUnstartedServer(router) + ts.EnableHTTP2 = true + ts.StartTLS() + t.Cleanup(ts.Close) + + rootCA := x509.NewCertPool() + rootCA.AddCert(ts.Certificate()) + + conn, err := grpc.NewClient(ts.Listener.Addr().String(), + grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{RootCAs: rootCA})), + ) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, conn.Close()) }) + return rts.NewCheckServiceClient(conn) +} + +func buildBatchURL(baseURL, maxDepth string) string { + return fmt.Sprintf("%s%s?max-depth=%s", baseURL, check.BatchRoute, maxDepth) +} + +func TestCheckRESTHandler(t *testing.T) { + nspaces := []*namespace.Namespace{{ + Name: "ns", + }} + + reg := driver.NewSqliteTestRegistry(t, driver.WithNamespaces(nspaces)) + h := check.NewHandler(reg) + r := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(r) + ts := httptest.NewServer(r) + t.Cleanup(ts.Close) + + for _, suite := range []struct { + name string + base string + assertDenied responseAssertion + }{ + {name: "base", base: check.RouteBase, assertDenied: baseAssertDenied}, + {name: "openapi", base: check.OpenAPIRouteBase, assertDenied: openAPIAssertDenied}, + } { + t.Run("suite="+suite.name, func(t *testing.T) { + assertDenied := suite.assertDenied + + t.Run("case=returns bad request on malformed int", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + suite.base + "?max-depth=foo") + require.NoError(t, err) + + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, string(readBody(t, resp)), "invalid syntax") + }) + + t.Run("case=returns bad request on malformed input", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + suite.base + "?" + url.Values{ + "subject": {"not#a valid userset rewrite"}, + }.Encode()) + require.NoError(t, err) + + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("case=returns bad request on missing subject", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + suite.base) + require.NoError(t, err) + + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, string(readBody(t, resp)), "subject is not allowed to be nil") + }) + + t.Run("case=returns denied on unknown namespace", func(t *testing.T) { + rt := testhelpers.APITupleFromString(t, "not:o#r@s") + resp, err := ts.Client().Get(ts.URL + suite.base + "?" + rt.ToURLQuery().Encode()) + require.NoError(t, err) + + assertDenied(t, resp) + }) + + t.Run("case=returns allowed", func(t *testing.T) { + rt := testhelpers.APITupleFromString(t, "ns:o#r@s") + testhelpers.MapAndInsertTuples(t, reg, rt) + + q := rt.ToURLQuery() + resp, err := ts.Client().Get(ts.URL + suite.base + "?" + q.Encode()) + require.NoError(t, err) + + assertAllowed(t, resp) + }) + + t.Run("case=returns denied", func(t *testing.T) { + rt := testhelpers.APITupleFromString(t, "ns:foo#r@s") + resp, err := ts.Client().Get(ts.URL + suite.base + "?" + rt.ToURLQuery().Encode()) + require.NoError(t, err) + + assertDenied(t, resp) + }) + }) + } +} + +func TestBatchCheckHandler(t *testing.T) { + nspaces := []*namespace.Namespace{{Name: "ns"}} + reg := driver.NewSqliteTestRegistry(t, driver.WithNamespaces(nspaces)) + h := check.NewHandler(reg) + + rt := testhelpers.APITupleFromString(t, "ns:o#r@s") + testhelpers.MapAndInsertTuples(t, reg, rt) + + // REST setup. + router := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(router) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + // gRPC setup. + checkClient := newTestGRPCCheckClient(t, h) + + t.Run("case=REST: validates request format", func(t *testing.T) { + t.Run("non-int max depth", func(t *testing.T) { + resp, err := ts.Client().Post(ts.URL+check.BatchRoute+"?max-depth=foo", + "application/json", nil) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, string(readBody(t, resp)), "invalid syntax") + }) + + t.Run("invalid JSON body", func(t *testing.T) { + resp, err := ts.Client().Post(buildBatchURL(ts.URL, "5"), + "application/json", strings.NewReader("not-json")) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, string(readBody(t, resp)), "could not unmarshal json") + }) + }) + + t.Run("case=validates max batch size", func(t *testing.T) { + t.Run("protocol=REST", func(t *testing.T) { + tuples := make([]client.Relationship, 11) + for i := range tuples { + tuples[i] = relationshipFromAPITuple(testhelpers.APITupleFromString(t, "ns:o#r@s")) + } + bodyBytes, err := json.Marshal(client.BatchCheckPermissionBody{Tuples: tuples}) + require.NoError(t, err) + resp, err := ts.Client().Post(buildBatchURL(ts.URL, "5"), "application/json", bytes.NewReader(bodyBytes)) + require.NoError(t, err) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Contains(t, string(readBody(t, resp)), "batch exceeds max size of 10") + }) + + t.Run("protocol=gRPC", func(t *testing.T) { + tuples := make([]*rts.RelationTuple, 11) + for i := range tuples { + tuples[i] = testhelpers.APITupleFromString(t, "ns:o#r@s").ToProto() + } + _, err := checkClient.BatchCheck(t.Context(), &rts.BatchCheckRequest{Tuples: tuples, MaxDepth: 5}) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.InvalidArgument, st.Code()) + require.Equal(t, "batch exceeds max size of 10", st.Message()) + }) + }) + + t.Run("case=returns correct results per tuple", func(t *testing.T) { + // Three tuples: allowed, not-allowed, unknown namespace. + restTuples := []client.Relationship{ + relationshipFromAPITuple(testhelpers.APITupleFromString(t, "ns:o#r@s")), // allowed + relationshipFromAPITuple(testhelpers.APITupleFromString(t, "ns:o2#r@s")), // not allowed + relationshipFromAPITuple(testhelpers.APITupleFromString(t, "n2:o#r@s")), // unknown namespace + } + grpcTuples := []*rts.RelationTuple{ + testhelpers.APITupleFromString(t, "ns:o#r@s").ToProto(), // allowed + testhelpers.APITupleFromString(t, "ns:o2#r@s").ToProto(), // not allowed + testhelpers.APITupleFromString(t, "n2:o#r@s").ToProto(), // unknown namespace + } + + t.Run("protocol=REST", func(t *testing.T) { + bodyBytes, err := json.Marshal(client.BatchCheckPermissionBody{Tuples: restTuples}) + require.NoError(t, err) + resp, err := ts.Client().Post(buildBatchURL(ts.URL, "5"), "application/json", bytes.NewReader(bodyBytes)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + var result client.BatchCheckPermissionResult + require.NoError(t, json.Unmarshal(readBody(t, resp), &result)) + require.Equal(t, client.BatchCheckPermissionResult{ + Results: []client.CheckPermissionResultWithError{ + {Allowed: true}, + {Allowed: false}, + {Allowed: false, Error: new("The requested resource could not be found")}, + }, + }, result) + }) + + t.Run("protocol=gRPC", func(t *testing.T) { + resp, err := checkClient.BatchCheck(t.Context(), &rts.BatchCheckRequest{Tuples: grpcTuples, MaxDepth: 5}) + require.NoError(t, err) + require.Len(t, resp.Results, 3) + require.True(t, resp.Results[0].Allowed) + require.Empty(t, resp.Results[0].Error) + require.False(t, resp.Results[1].Allowed) + require.Empty(t, resp.Results[1].Error) + require.False(t, resp.Results[2].Allowed) + require.Equal(t, "The requested resource could not be found", resp.Results[2].Error) + }) + }) +} + +// relationshipFromAPITuple converts a ketoapi.RelationTuple to a client.Relationship +// for use in REST batch check requests. +func relationshipFromAPITuple(tuple *ketoapi.RelationTuple) client.Relationship { + rel := client.Relationship{ + Namespace: tuple.Namespace, + Object: tuple.Object, + Relation: tuple.Relation, + } + if tuple.SubjectID != nil { + rel.SubjectId = tuple.SubjectID + } else if tuple.SubjectSet != nil { + rel.SubjectSet = &client.SubjectSet{ + Namespace: tuple.SubjectSet.Namespace, + Object: tuple.SubjectSet.Object, + Relation: tuple.SubjectSet.Relation, + } + } + return rel +} + +func TestCheckStrictModeLimit(t *testing.T) { + depthLimitScenario.WithStrict(true).Run(t, func(t *testing.T, reg driver.Registry) { + require.NoError(t, reg.Config(t.Context()).Set(config.KeyLimitMaxReadDepth, 2)) + h := check.NewHandler(reg) + + // This tuple has a depth of 3, which exceeds the configured limit of 2, causing to hit the limitation. + tupleQuery := testhelpers.APITupleFromString(t, "Resource:doc#union@User:Alice").ToURLQuery() + + t.Run("case=HTTP: returns 422 with limitation kind", func(t *testing.T) { + router := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(router) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + resp, err := ts.Client().Get(ts.URL + check.RouteBase + "?max-depth=2&" + tupleQuery.Encode()) + require.NoError(t, err) + body := readBody(t, resp) + + require.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode, "%s", body) + require.Equal(t, "The check could not be completed", gjson.GetBytes(body, "error.message").String()) + require.Contains(t, gjson.GetBytes(body, "error.reason").String(), string(check.LimitationMaxDepthExceeded)) + }) + + t.Run("case=gRPC: returns FailedPrecondition with limitation kind", func(t *testing.T) { + checkClient := newTestGRPCCheckClient(t, h) + tuple := testhelpers.APITupleFromString(t, "Resource:doc#union@User:Alice") + _, err := checkClient.Check(t.Context(), &rts.CheckRequest{ + Tuple: tuple.ToProto(), + MaxDepth: 2, + }) + require.Error(t, err) + + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.FailedPrecondition, st.Code()) + require.Equal(t, "The check could not be completed", st.Message()) + + var ei *errdetails.ErrorInfo + for _, d := range st.Details() { + if info, ok := d.(*errdetails.ErrorInfo); ok { + ei = info + break + } + } + require.NotNilf(t, ei, "%+v", st.Details()) + require.Contains(t, ei.Reason, string(check.LimitationMaxDepthExceeded)) + }) + }) +} + +func TestBatchCheckLimitHandler(t *testing.T) { + for _, scenario := range []testhelpers.Scenario{ + widthLimitScenario, + widthLimitScenario.WithStrict(true), + } { + scenario.Run(t, func(t *testing.T, reg driver.Registry) { + require.NoError(t, reg.Config(t.Context()).Set(config.KeyLimitMaxReadWidth, 1)) + h := check.NewHandler(reg) + strict := scenario.Strict + + // Bob is only in the truncated branch → MembershipUnknown at width=1. + tuple := testhelpers.APITupleFromString(t, "Resource:doc#readers@User:Bob") + + t.Run("case=REST: strict mode surfaces limitation in error field", func(t *testing.T) { + router := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(router) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + + bodyBytes, err := json.Marshal(client.BatchCheckPermissionBody{ + Tuples: []client.Relationship{relationshipFromAPITuple(tuple)}, + }) + require.NoError(t, err) + + resp, err := ts.Client().Post( + fmt.Sprintf("%s%s?max-depth=10", ts.URL, check.BatchRoute), + "application/json", bytes.NewReader(bodyBytes)) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + var result client.BatchCheckPermissionResult + require.NoError(t, json.Unmarshal(readBody(t, resp), &result)) + require.Len(t, result.Results, 1) + require.False(t, result.Results[0].Allowed) + if strict { + require.NotNil(t, result.Results[0].Error) + require.Contains(t, *result.Results[0].Error, string(check.LimitationMaxWidthExceeded)) + } else { + require.Empty(t, result.Results[0].Error) + } + }) + + t.Run("case=gRPC: strict mode surfaces limitation in error field", func(t *testing.T) { + checkClient := newTestGRPCCheckClient(t, h) + resp, err := checkClient.BatchCheck(t.Context(), &rts.BatchCheckRequest{ + Tuples: []*rts.RelationTuple{tuple.ToProto()}, + MaxDepth: 10, + }) + require.NoError(t, err) + require.Len(t, resp.Results, 1) + require.False(t, resp.Results[0].Allowed) + if strict { + require.Contains(t, resp.Results[0].Error, string(check.LimitationMaxWidthExceeded)) + } else { + require.Empty(t, resp.Results[0].Error) + } + }) + }) + } +} diff --git a/internal/check/rewrites_test.go b/internal/check/rewrites_test.go new file mode 100644 index 0000000..bbbe6a9 --- /dev/null +++ b/internal/check/rewrites_test.go @@ -0,0 +1,284 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/goleak" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/trace" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/testhelpers" +) + +var rewrites = testhelpers.Scenario{ + Name: "userset rewrites", + Opl: ` + class User implements Namespace {} + + class Doc implements Namespace { + related: { + owner: User[] + parent: Doc[] + } + permits = { + editor: (ctx: Context) => this.related.owner.includes(ctx.subject), + viewer: (ctx: Context) => this.permits.editor(ctx) || + this.related.parent.traverse(p => p.permits.viewer(ctx)) + } + } + + class Group implements Namespace { + related: { + member: User[] + } + } + + class Level implements Namespace { + related: { + member: User[] + } + } + + class Resource implements Namespace { + related: { + level: Level[] + owner: Group[] + } + permits = { + viewer: (ctx: Context) => this.related.owner.traverse(g => g.related.member.includes(ctx.subject)), + isOwner: (ctx: Context) => this.related.owner.traverse(g => g.related.member.includes(ctx.subject)), + + read: (ctx: Context) => this.permits.viewer(ctx) || this.permits.isOwner(ctx), + update: (ctx: Context) => this.permits.isOwner(ctx), + delete: (ctx: Context) => this.permits.isOwner(ctx) && this.related.level.traverse(l => l.related.member.includes(ctx.subject)), + } + } + + class Acl implements Namespace { + related: { + allow: User[] + deny: User[] + } + permits = { + access: (ctx: Context) => this.related.allow.includes(ctx.subject) && !this.related.deny.includes(ctx.subject) + } + } + `, + InputTuples: []string{ + // Direct ownership — plain and namespace-qualified subjects both work. + "Doc:document#owner@plain_user", + "Doc:document#owner@User:user", + + // Single-level parent: doc_in_folder inherits viewer from its parent folder. + "Doc:folder#owner@plain_user", + "Doc:folder#owner@User:user", + "Doc:doc_in_folder#parent@Doc:folder", + + // Deep hierarchy: user owns folder_a; viewer propagates down 4 levels to file. + "Doc:folder_a#owner@user", + "Doc:folder_b#parent@Doc:folder_a", + "Doc:folder_c#parent@Doc:folder_b", + "Doc:file#parent@Doc:folder_c", + + // Group + level gating: delete requires owner-group membership AND correct level. + "Group:editors#member@mark", + "Group:editors#member@mike", + "Level:superadmin#member@mark", + "Level:superadmin#member@sandy", + "Resource:topsecret#owner@Group:editors#", + "Resource:topsecret#level@Level:superadmin#", + + // Allow/deny ACL: deny overrides allow (mallory is on both lists). + "Acl:document#allow@alice", + "Acl:document#allow@bob", + "Acl:document#allow@mallory", + "Acl:document#deny@mallory", + }, +} + +type path []string + +func TestUsersetRewrites(t *testing.T) { + testCases := []struct { + query string + expected check.Result + expectedPaths []path + }{ + { + // direct + query: "Doc:document#owner@User:user", + expected: check.ResultIsMember, + }, + { + // userset rewrite + query: "Doc:document#editor@User:user", + expected: check.ResultIsMember, + }, + { + // userset rewrite + query: "Doc:document#editor@plain_user", + expected: check.ResultIsMember, + }, + { + // transitive userset rewrite + query: "Doc:document#viewer@User:user", + expected: check.ResultIsMember, + }, + { + query: "Doc:document#editor@nobody", + expected: check.ResultNotMember, + }, + { + query: "Doc:folder#viewer@User:user", + expected: check.ResultIsMember, + }, + { + // tuple to userset + query: "Doc:doc_in_folder#viewer@User:user", + expected: check.ResultIsMember, + }, + { + // tuple to userset + query: "Doc:doc_in_folder#viewer@plain_user", + expected: check.ResultIsMember, + }, + { + // tuple to userset + query: "Doc:doc_in_folder#viewer@nobody", + expected: check.ResultNotMember, + }, + { + // tuple to userset + query: "Doc:another_doc#viewer@user", + expected: check.ResultNotMember, + }, + { + query: "Doc:file#viewer@user", + expected: check.ResultIsMember, + }, + { + query: "Level:superadmin#member@mark", + expected: check.ResultIsMember, // mark is both editor and has correct level + }, + { + query: "Resource:topsecret#isOwner@mark", + expected: check.ResultIsMember, // mark is both editor and has correct level + }, + { + query: "Resource:topsecret#delete@mark", + expected: check.ResultIsMember, // mark is both editor and has correct level + expectedPaths: []path{ + {"*", "Resource:topsecret#delete@mark", "*", "Level:superadmin#member@mark"}, + {"*", "Resource:topsecret#delete@mark", "Resource:topsecret#isOwner@mark", "*", "*", "*", "Group:editors#member@mark"}, + }, + }, + { + query: "Resource:topsecret#update@mike", + expected: check.ResultIsMember, // mike owns the resource + }, + { + query: "Level:superadmin#member@mike", + expected: check.ResultNotMember, // mike does not have correct level + }, + { + query: "Resource:topsecret#delete@mike", + expected: check.ResultNotMember, // mike does not have correct level + }, + { + query: "Resource:topsecret#delete@sandy", + expected: check.ResultNotMember, // sandy is not in the editor Group + }, + { + query: "Acl:document#access@alice", + expected: check.ResultIsMember, + expectedPaths: []path{{"*", "Acl:document#access@alice", "Acl:document#allow@alice"}}, + }, + { + query: "Acl:document#access@bob", + expected: check.ResultIsMember, + }, + { + query: "Acl:document#allow@mallory", + expected: check.ResultIsMember, + }, + { + query: "Acl:document#access@mallory", + expected: check.ResultNotMember, // mallory is also on deny-list + }, + } + + rewrites.Run(t, func(t *testing.T, reg driver.Registry) { + e := trace.NewEngine(reg) + defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) + + t.Run("suite=testcases", func(t *testing.T) { + defer goleak.VerifyNone(t, goleak.IgnoreCurrent()) + + for _, tc := range testCases { + t.Run("case="+tc.query, func(t *testing.T) { + rt := testhelpers.TupleFromString(t, tc.query) + + res, tree := e.CheckRelationTupleWithTrace(t.Context(), rt, 100) + require.NoError(t, res.Err) + assert.Equal(t, tc.expected.Membership.String(), res.Membership.String()) + + if len(tc.expectedPaths) > 0 { + for _, path := range tc.expectedPaths { + assertPath(t, path, tree) + } + } + }) + } + }) + t.Run("suite=one worker", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + e := trace.NewEngine(reg) + + rt := testhelpers.TupleFromString(t, "Doc:file#viewer@user") + res, _ := e.CheckRelationTupleWithTrace(ctx, rt, 100) + require.NoError(t, res.Err) + assert.Equal(t, check.ResultIsMember.Membership, res.Membership) + }) + }) +} + +// assertPath asserts that the given path can be found in the tree. +func assertPath(t *testing.T, path path, tree *trace.Node) { + require.NotNil(t, tree) + assert.True(t, hasPath(t, path, tree), "could not find path %s in tree:\n%s", path, tree) +} + +func hasPath(t *testing.T, path path, tree *trace.Node) bool { + if len(path) == 0 { + return true + } + treeLabel := tree.Tuple.String() + if path[0] != "*" { + // use testhelpers.TupleFromString to compare against paths with UUIDs. + tuple := testhelpers.TupleFromString(t, path[0]) + tupleStr := tuple.String() + if tupleStr != treeLabel { + return false + } + } + + if len(path) == 1 { + return true + } + + for _, child := range tree.Children { + if hasPath(t, path[1:], child) { + return true + } + } + return false +} diff --git a/internal/check/step.go b/internal/check/step.go new file mode 100644 index 0000000..a81a5d7 --- /dev/null +++ b/internal/check/step.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "context" +) + +// StepKind identifies the execution model type of a Step. +type StepKind string + +const ( + StepIsAllowed StepKind = "is_allowed" + StepDirect StepKind = "direct" + StepDirectMulti StepKind = "direct_multi" + StepExpand StepKind = "expand_subject" + StepComputed StepKind = "computed" + StepTraverse StepKind = "traverse" + StepRewrite StepKind = "rewrite" + StepInvert StepKind = "invert" +) + +// Step is a single unit of execution within the permission check engine. +type Step interface { + // Kind returns the execution-model kind of this step. + Kind() StepKind + + // Execute performs the step's work and returns the membership result. + // Child steps must be dispatched through ex, not called directly. + Execute(ctx context.Context, req CheckRequest, ex Executor) Result +} diff --git a/internal/check/step/call_limit.go b/internal/check/step/call_limit.go new file mode 100644 index 0000000..aabce4b --- /dev/null +++ b/internal/check/step/call_limit.go @@ -0,0 +1,55 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + "net/http" + + "github.com/ory/herodot" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + + "github.com/ory/keto/internal/check" +) + +type callBudgetKeyType struct{} + +var callBudgetKey callBudgetKeyType + +// maxCallSteps bounds the number of steps along a single resolution path. Most +// recursion is already bounded by RestDepth, but same-object computed hops +// (ComputedUsersetStep, InvertStep) don't decrement it, so a cyclic config +// (e.g. two permits that reference each other) would otherwise recurse until +// the stack overflows and crashes the process. This is a blanket per-path +// ceiling, well above any real config and well below a stack overflow. +const maxCallSteps = 1000 + +func errCallBudgetExceeded() error { + return errors.WithStack(&herodot.DefaultError{ + StatusField: http.StatusText(http.StatusUnprocessableEntity), + ErrorField: "The check could not be completed", + ReasonField: "The check was aborted after too many resolution steps. This usually indicates a cyclic permit reference in the namespace configuration.", + CodeField: http.StatusUnprocessableEntity, + GRPCCodeField: codes.FailedPrecondition, + }) +} + +// CallLimitMiddleware aborts a check once any single resolution path exceeds +// maxCallSteps. It returns a decisive error so the whole check stops +// immediately, rather than a limitation (which is non-decisive and would let a +// cyclic config keep exploring exponentially many branches). The budget is +// carried per-path in the context and seeded lazily. +func CallLimitMiddleware() check.Middleware { + return func(ctx context.Context, s check.Step, req check.CheckRequest, next func(context.Context, check.Step, check.CheckRequest) check.Result) check.Result { + remaining, ok := ctx.Value(callBudgetKey).(int) + if !ok { + remaining = maxCallSteps + } + if remaining <= 0 { + return check.Result{Err: errCallBudgetExceeded()} + } + return next(context.WithValue(ctx, callBudgetKey, remaining-1), s, req) + } +} diff --git a/internal/check/step/computed.go b/internal/check/step/computed.go new file mode 100644 index 0000000..888ad46 --- /dev/null +++ b/internal/check/step/computed.go @@ -0,0 +1,37 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/relationtuple" +) + +// ComputedUsersetStep rewrites the tuple's relation to the computed relation +// and delegates to IsAllowedStep. +type ComputedUsersetStep struct { + Relation string +} + +func (ComputedUsersetStep) Kind() check.StepKind { return check.StepComputed } + +func (s ComputedUsersetStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + WithField("computed subjectSet relation", s.Relation). + Trace("check computed subjectSet") + + childReq := check.CheckRequest{ + Tuple: &relationtuple.RelationTuple{ + Namespace: req.Tuple.Namespace, + Object: req.Tuple.Object, + Relation: s.Relation, + Subject: req.Tuple.Subject, + }, + RestDepth: req.RestDepth, + } + return ex.Run(ctx, IsAllowedStep{}, childReq) +} diff --git a/internal/check/step/direct.go b/internal/check/step/direct.go new file mode 100644 index 0000000..524fb13 --- /dev/null +++ b/internal/check/step/direct.go @@ -0,0 +1,41 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ory/keto/internal/check" +) + +// DirectStep checks whether the exact relation tuple exists in the database. +type DirectStep struct{} + +func (DirectStep) Kind() check.StepKind { return check.StepDirect } + +func (DirectStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Trace("check direct") + + found, err := ex.Deps().RelationTupleManager().ExistsRelationTuples(ctx, req.Tuple.ToQuery()) + switch { + case err != nil: + ex.Deps().Logger(). + WithField("method", "DirectStep"). + WithError(err). + Error("failed to look up direct access in db") + return check.Result{Err: errors.WithStack(err)} + case found: + return check.ResultIsMember + default: + return check.ResultNotMember + } +} diff --git a/internal/check/step/direct_multi.go b/internal/check/step/direct_multi.go new file mode 100644 index 0000000..b4fa9be --- /dev/null +++ b/internal/check/step/direct_multi.go @@ -0,0 +1,40 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ory/keto/internal/check" +) + +// DirectMultiStep checks whether any of the given relations exist directly in +// the database for the tuple. +type DirectMultiStep struct { + Relations []string +} + +func (DirectMultiStep) Kind() check.StepKind { return check.StepDirectMulti } + +func (s DirectMultiStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + WithField("relations", s.Relations). + Trace("check direct multi") + + res, err := ex.Deps().RelationTupleManager().FindTupleWithRelations(ctx, req.Tuple, s.Relations) + if err != nil { + return check.Result{Err: errors.WithStack(err)} + } + if res != nil { + return check.ResultIsMember + } + return check.ResultNotMember +} diff --git a/internal/check/step/dispatch.go b/internal/check/step/dispatch.go new file mode 100644 index 0000000..1679122 --- /dev/null +++ b/internal/check/step/dispatch.go @@ -0,0 +1,91 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/keto/internal/check" +) + +// IsAllowedStep is the central semantic dispatcher. It reads the AST rule for +// the tuple and determines what checks are applicable via RunUnion. +type IsAllowedStep struct { + skipDirect bool +} + +func (IsAllowedStep) Kind() check.StepKind { return check.StepIsAllowed } + +func (s IsAllowedStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Trace("check is allowed") + + nm, err := ex.Deps().Config(ctx).NamespaceManager() + if err != nil { + return check.Result{Err: err} + } + + relation, err := astRelationFor(ctx, nm, req.Tuple) + if err != nil { + return check.Result{Err: err} + } + + hasRewrite := relation != nil && relation.SubjectSetRewrite != nil + strictMode := ex.Deps().Config(ctx).StrictMode() + + // In strict mode, only run steps that OPL explicitly permits. + // In non-strict mode, run all applicable steps for backwards compatibility. + canHaveDirect := !strictMode || AllowsDirectMember(relation, req.Tuple.Subject) + + steps := make([]check.PlannedStep, 0, 3) + + if hasRewrite { + steps = append(steps, check.PlannedStep{ + Step: RewriteStep{Rewrite: relation.SubjectSetRewrite}, + Req: req, + }) + } + + if canHaveDirect && !s.skipDirect { + steps = append(steps, check.PlannedStep{ + Step: DirectStep{}, + Req: req, + }) + } + + // In strict mode, only expand when OPL explicitly declares subject-set types, and + // filter expansion to only those declared types. + // In non-strict mode, always expand to preserve backwards compatibility. + if strictMode { + subjectSetTypes, err := subjectSetTypesFor(ctx, nm, req.Tuple.Subject, relation) + if err != nil { + return check.Result{Err: err} + } + if len(subjectSetTypes) > 0 { + steps = append(steps, check.PlannedStep{ + Step: ExpandSubjectStep{SubjectSetTypes: subjectSetTypes}, + Req: req, + }) + } + } else { + steps = append(steps, check.PlannedStep{ + Step: ExpandSubjectStep{}, + Req: req, + }) + } + + switch len(steps) { + case 0: + return check.ResultNotMember + case 1: + return ex.Run(ctx, steps[0].Step, steps[0].Req) + default: + return ex.RunUnion(ctx, steps...) + } +} diff --git a/internal/check/step/executor.go b/internal/check/step/executor.go new file mode 100644 index 0000000..6e5c31d --- /dev/null +++ b/internal/check/step/executor.go @@ -0,0 +1,277 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + "sync" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/relationtuple" +) + +type ( + relationTuple = relationtuple.RelationTuple + query = relationtuple.RelationQuery +) + +// runMode defines the decisiveness and default result semantics for an execution mode. +// +// IsDecisive determines whether a step result ends execution early (short-circuit). +// - Union: IsMember or any Error is decisive. +// - Intersection: NotMember or any Error is decisive. +// +// NoDecisionResult is returned when all steps ran without triggering a short-circuit. +// - Union: NotMember (no member found among alternatives). +// - Intersection: IsMember (all conditions met). +// +// NoStepsResult is returned when no steps were added (empty input slice). +// - Both: NotMember (no tuples to check). +type runMode interface { + IsDecisive(check.Result) bool + NoDecisionResult() check.Result + NoStepsResult() check.Result +} + +// unionRunMode implements OR semantics: first IsMember or Error wins. +type unionRunMode struct{} + +func (unionRunMode) IsDecisive(r check.Result) bool { + return r.Membership == check.IsMember || r.Err != nil +} +func (unionRunMode) NoDecisionResult() check.Result { return check.ResultNotMember } +func (unionRunMode) NoStepsResult() check.Result { return check.ResultNotMember } + +// intersectionRunMode implements AND semantics: first NotMember or Error wins. +type intersectionRunMode struct{} + +func (intersectionRunMode) IsDecisive(r check.Result) bool { + return r.Membership == check.NotMember || r.Err != nil +} +func (intersectionRunMode) NoDecisionResult() check.Result { return check.ResultIsMember } +func (intersectionRunMode) NoStepsResult() check.Result { return check.ResultNotMember } + +var ( + unionMode runMode = unionRunMode{} + intersectionMode runMode = intersectionRunMode{} +) + +// Executor implements check.Executor and runs Steps with short-circuit +// union/intersection semantics and a middleware chain. +type Executor struct { + dep check.EngineDependencies + middlewares []check.Middleware +} + +func NewExecutor(dep check.EngineDependencies, middlewares ...check.Middleware) *Executor { + return &Executor{dep: dep, middlewares: middlewares} +} + +func (e *Executor) Deps() check.EngineDependencies { return e.dep } + +func (e *Executor) Run(ctx context.Context, s check.Step, req check.CheckRequest) check.Result { + return e.runAt(ctx, s, req, 0) +} + +// runAt executes the middleware chain at position i, then the step's Execute method. +// It implements a functional middleware chain: each middleware receives a next() function +// that continues the chain. +func (e *Executor) runAt(ctx context.Context, s check.Step, req check.CheckRequest, i int) check.Result { + if i >= len(e.middlewares) { + return s.Execute(ctx, req, e) + } + return e.middlewares[i](ctx, s, req, func(ctx context.Context, s check.Step, req check.CheckRequest) check.Result { + return e.runAt(ctx, s, req, i+1) + }) +} + +func (e *Executor) RunUnion(ctx context.Context, steps ...check.PlannedStep) check.Result { + return runAll(ctx, e, unionMode, 1, steps...) +} + +func (e *Executor) RunIntersection(ctx context.Context, steps ...check.PlannedStep) check.Result { + return runAll(ctx, e, intersectionMode, 1, steps...) +} + +func (e *Executor) NewUnionRunner(ctx context.Context) check.StepRunner { + return newStepGroup(ctx, 1, unionMode, e) +} + +// CheckRelationTuple checks if the tuple's subject has the relation on the object. +func (e *Executor) CheckRelationTuple(ctx context.Context, r *relationTuple, restDepth int) check.Result { + restDepth = clampDepth(ctx, e.dep, restDepth) + return e.Run(ctx, IsAllowedStep{}, check.CheckRequest{ + Tuple: r, + RestDepth: restDepth, + }) +} + +func clampDepth(ctx context.Context, c config.Provider, restDepth int) int { + if globalMaxDepth := c.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth { + return globalMaxDepth + } + return restDepth +} + +func runAll(ctx context.Context, ex check.Executor, mode runMode, limit int, steps ...check.PlannedStep) check.Result { + if len(steps) == 0 { + return mode.NoStepsResult() + } + g := newStepGroup(ctx, limit, mode, ex) + + for _, ps := range steps { + if g.Done() { + break + } + g.Add(ps) + } + return g.Result() +} + +// stepGroup runs sub-checks with short-circuit semantics. When sequential (limit=1) +// steps run inline with no goroutine overhead. When limit>1 steps run +// concurrently, cancelling siblings on the first decisive result. +// stepGroup implements check.StepRunner. +type stepGroup struct { + ctx context.Context + cancel context.CancelFunc + ex check.Executor + sequential bool + + sem chan struct{} + wg sync.WaitGroup + + mu sync.Mutex + result check.Result + set bool + limitation check.LimitationKind + + mode runMode +} + +// newStepGroup creates a StepGroup that runs steps with the given concurrencyLimit and mode. +// If concurrencyLimit < 1, it's clamped to 1 (sequential). Sequential execution (concurrencyLimit=1) +// runs steps inline without goroutines; concurrent execution spawns workers up to +// concurrencyLimit. The returned context is derived from ctx via WithCancel and is cancelled +// when a decisive result is reached (triggering sibling cancellation). +func newStepGroup(ctx context.Context, concurrencyLimit int, mode runMode, ex check.Executor) *stepGroup { + if concurrencyLimit < 1 { + concurrencyLimit = 1 + } + g := &stepGroup{ + ex: ex, + sequential: concurrencyLimit == 1, + mode: mode, + } + g.ctx, g.cancel = context.WithCancel(ctx) + if !g.sequential { + g.sem = make(chan struct{}, concurrencyLimit) + } + return g +} + +// Add submits ps for execution. No-op if Done returns true. +func (g *stepGroup) Add(ps check.PlannedStep) { + g.add(func(ctx context.Context) check.Result { + return g.ex.Run(ctx, ps.Step, ps.Req) + }) +} + +func (g *stepGroup) add(f func(context.Context) check.Result) { + if g.sequential { + if g.Done() { + return + } + g.handle(f(g.ctx)) + return + } + + // currently dead path as we always use concurrentLimit = 1 + select { + case g.sem <- struct{}{}: + case <-g.ctx.Done(): + return + } + if g.Done() { + <-g.sem + return + } + g.wg.Add(1) + go func() { + defer g.wg.Done() + defer func() { <-g.sem }() + g.handle(f(g.ctx)) + }() +} + +func (g *stepGroup) AddLimitation(l check.LimitationKind) { + g.mu.Lock() + if !g.set && g.limitation == "" { + g.limitation = l + } + g.mu.Unlock() +} + +func (g *stepGroup) handle(r check.Result) { + if g.mode.IsDecisive(r) && g.trySetResult(r) { + g.cancel() + return + } + if r.Membership == check.MembershipUnknown && r.Limitation != "" { + g.AddLimitation(r.Limitation) + } +} + +// Done reports whether a decisive result has been reached. +func (g *stepGroup) Done() bool { + select { + case <-g.ctx.Done(): + return true + default: + return false + } +} + +// Ctx returns the group's context. +func (g *stepGroup) Ctx() context.Context { return g.ctx } + +// Result waits for all in-flight steps to complete and returns the aggregated result. +// Safe to call multiple times; subsequent calls return the same value. +func (g *stepGroup) Result() check.Result { + g.wg.Wait() + // Capture ctx.Err() before g.cancel() to distinguish an external cancellation + // (deadline exceeded, parent cancelled) from the internal cancel used to signal siblings. + ctxErr := g.ctx.Err() + g.cancel() + g.mu.Lock() + defer g.mu.Unlock() + + // No decisive result was set. Return in priority order: + // 1. Context cancelled by parent or sibling — propagate the error. + // 2. A limitation was recorded — result is indeterminate. + // 3. Neither — no step was decisive, so fall back to the mode default. + if !g.set { + if ctxErr != nil { + g.result = check.Result{Membership: check.MembershipUnknown, Err: ctxErr} + } else if g.limitation != "" { + g.result = check.Result{Membership: check.MembershipUnknown, Limitation: g.limitation} + } else { + g.result = g.mode.NoDecisionResult() + } + g.set = true + } + return g.result +} + +func (g *stepGroup) trySetResult(r check.Result) bool { + g.mu.Lock() + defer g.mu.Unlock() + if g.set { + return false + } + g.set = true + g.result = r + return true +} diff --git a/internal/check/step/executor_test.go b/internal/check/step/executor_test.go new file mode 100644 index 0000000..21d81fe --- /dev/null +++ b/internal/check/step/executor_test.go @@ -0,0 +1,587 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + "errors" + "sync" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/check" +) + +func TestBaseExecutorRunSequential(t *testing.T) { + t.Parallel() + + sentinel := errors.New("planted error") + unknownErr := check.Result{Membership: check.MembershipUnknown, Err: sentinel} + unknownLimit := check.Result{Membership: check.MembershipUnknown, Limitation: check.LimitationMaxDepthExceeded} + + t.Run("RunUnion", func(t *testing.T) { + t.Parallel() + + t.Run("NotMember when there is no step", func(t *testing.T) { + t.Parallel() + result := NewExecutor(nil).RunUnion(t.Context()) + assert.Equal(t, check.ResultNotMember, result) + }) + + t.Run("return error when called after context is canceled", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + calls := make([]int, 0, 1) + steps := plannedSteps([]check.Result{check.ResultIsMember}, &calls) + result := NewExecutor(nil).RunUnion(ctx, steps...) + + // context is cancelled, therefore steps did not run, and the result Unknown + assert.Equal(t, check.MembershipUnknown, result.Membership) + assert.ErrorIs(t, result.Err, context.Canceled) + }) + + t.Run("table", func(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + results []check.Result + expectedResult check.Result + expectedErr error + expectedCalls []int + } + + for _, tc := range []testCase{ + { + name: "runs all steps when none is member", + results: []check.Result{check.ResultUnknown, check.ResultNotMember, check.ResultUnknown, check.ResultNotMember, check.ResultNotMember}, + // todo: ideally, this should be Unknown + expectedResult: check.ResultNotMember, + expectedCalls: []int{0, 1, 2, 3, 4}, + }, + { + name: "stops on first Member", + results: []check.Result{check.ResultNotMember, check.ResultIsMember, check.ResultNotMember, check.ResultNotMember}, + expectedResult: check.ResultIsMember, + expectedCalls: []int{0, 1}, + }, + { + name: "stops on first Member; last step is Member", + results: []check.Result{check.ResultNotMember, check.ResultNotMember, check.ResultNotMember, check.ResultIsMember}, + expectedResult: check.ResultIsMember, + expectedCalls: []int{0, 1, 2, 3}, + }, + { + name: "stops on first Member; therefore doesn't run error after Member", + results: []check.Result{check.ResultNotMember, check.ResultIsMember, {Err: sentinel}, check.ResultNotMember}, + expectedResult: check.ResultIsMember, + expectedCalls: []int{0, 1}, + }, + { + name: "error short-circuits", + results: []check.Result{check.ResultNotMember, {Err: sentinel}, check.ResultNotMember, check.ResultNotMember}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0, 1}, + }, + { + name: "NotMember + Error = Unknown+error", + results: []check.Result{check.ResultNotMember, unknownErr}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0, 1}, + }, + { + name: "should stop on first error", + results: []check.Result{unknownErr, check.ResultIsMember}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0}, + }, + { + name: "NotMember + limit = Unknown", + results: []check.Result{check.ResultNotMember, unknownLimit}, + expectedResult: unknownLimit, + expectedCalls: []int{0, 1}, + }, + { + name: "Limit + NotMember = Unknown", + results: []check.Result{unknownLimit, check.ResultNotMember}, + expectedResult: unknownLimit, + expectedCalls: []int{0, 1}, + }, + { + name: "Limit + Member = Member", + results: []check.Result{unknownLimit, check.ResultIsMember}, + expectedResult: check.ResultIsMember, + expectedCalls: []int{0, 1}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + calls := make([]int, 0, len(tc.results)) + steps := plannedSteps(tc.results, &calls) + + result := NewExecutor(nil).RunUnion(t.Context(), steps...) + assert.Equal(t, tc.expectedResult.Membership, result.Membership) + assert.ErrorIs(t, result.Err, tc.expectedErr) + assert.Equal(t, tc.expectedCalls, calls) + }) + } + }) + }) + + t.Run("RunIntersection", func(t *testing.T) { + t.Parallel() + + t.Run("NotMember when there is no step", func(t *testing.T) { + t.Parallel() + result := NewExecutor(nil).RunIntersection(t.Context()) + assert.Equal(t, check.ResultNotMember, result) + }) + + t.Run("return error when called after context is canceled", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + calls := make([]int, 0, 1) + steps := plannedSteps([]check.Result{check.ResultIsMember}, &calls) + result := NewExecutor(nil).RunIntersection(ctx, steps...) + + // context is cancelled, therefore steps did not run, and the result Unknown + assert.Equal(t, check.MembershipUnknown, result.Membership) + assert.ErrorIs(t, result.Err, context.Canceled) + }) + + t.Run("table", func(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + results []check.Result + expectedResult check.Result + expectedErr error + expectedCalls []int + } + + for _, tc := range []testCase{ + { + name: "runs all steps when no NotMember", + results: []check.Result{check.ResultIsMember, check.ResultIsMember, check.ResultIsMember}, + expectedResult: check.ResultIsMember, + expectedCalls: []int{0, 1, 2}, + }, + { + name: "stops on first NotMember", + results: []check.Result{check.ResultIsMember, check.ResultNotMember, check.ResultIsMember, check.ResultIsMember}, + expectedResult: check.ResultNotMember, + expectedCalls: []int{0, 1}, + }, + { + name: "stops on first NotMember; last step is NotMember", + results: []check.Result{check.ResultIsMember, check.ResultIsMember, check.ResultIsMember, check.ResultNotMember}, + expectedResult: check.ResultNotMember, + expectedCalls: []int{0, 1, 2, 3}, + }, + { + name: "error short-circuits", + results: []check.Result{check.ResultIsMember, {Err: sentinel}, check.ResultIsMember, check.ResultIsMember}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0, 1}, + }, + { + name: "Member + Error = Unknown+error", + results: []check.Result{check.ResultIsMember, unknownErr}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0, 1}, + }, + { + name: "should stop on first error", + results: []check.Result{unknownErr, check.ResultIsMember}, + expectedResult: check.ResultUnknown, + expectedErr: sentinel, + expectedCalls: []int{0}, + }, + { + name: "Member + limit = Unknown", + results: []check.Result{check.ResultIsMember, unknownLimit}, + expectedResult: unknownLimit, + expectedCalls: []int{0, 1}, + }, + { + name: "Limit + Member = Unknown", + results: []check.Result{unknownLimit, check.ResultIsMember}, + expectedResult: unknownLimit, + expectedCalls: []int{0, 1}, + }, + { + name: "Limit + NotMember = NotMember", + results: []check.Result{unknownLimit, check.ResultNotMember}, + expectedResult: check.ResultNotMember, + expectedCalls: []int{0, 1}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + calls := make([]int, 0, len(tc.results)) + steps := plannedSteps(tc.results, &calls) + + result := NewExecutor(nil).RunIntersection(t.Context(), steps...) + assert.Equal(t, tc.expectedResult.Membership, result.Membership) + assert.Equal(t, tc.expectedResult.Limitation, result.Limitation) + assert.ErrorIs(t, result.Err, tc.expectedErr) + assert.Equal(t, tc.expectedCalls, calls) + }) + } + }) + }) +} + +func TestGroupSequential(t *testing.T) { + t.Parallel() + + t.Run("Add after decisive result is no-op", func(t *testing.T) { + t.Parallel() + + calls := make([]int, 0, 3) + runner := newStepGroup(t.Context(), 1, unionMode, NewExecutor(nil)) + + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 0) + return check.ResultNotMember + })}) + require.False(t, runner.Done()) + + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 1) + return check.ResultIsMember + })}) + require.True(t, runner.Done()) + + // this step is no-op because the runner is already done with a decisive result + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 2) + return check.ResultIsMember + })}) + + result := runner.Result() + require.Equal(t, check.IsMember, result.Membership) + assert.Equal(t, []int{0, 1}, calls) + }) + + t.Run("Add after cancelled context does not execute the step", func(t *testing.T) { + t.Parallel() + + calls := 0 + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + runner := newStepGroup(ctx, 1, unionMode, NewExecutor(nil)) + + require.True(t, runner.Done()) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls++ + return check.ResultIsMember + })}) + + assert.Equal(t, 0, calls) + }) + + t.Run("Result after cancelled context returns MembershipUnknown and context.Canceled", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + runner := newStepGroup(ctx, 1, unionMode, NewExecutor(nil)) + result := runner.Result() + assert.Equal(t, check.MembershipUnknown, result.Membership) + assert.ErrorIs(t, result.Err, context.Canceled) + }) + + t.Run("limit lower than one is clamped to sequential", func(t *testing.T) { + t.Parallel() + + for _, limit := range []int{0, -1} { + t.Run("limit", func(t *testing.T) { + t.Parallel() + + calls := make([]int, 0, 3) + runner := newStepGroup(t.Context(), limit, unionMode, NewExecutor(nil)) + + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 0) + return check.ResultNotMember + })}) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 1) + return check.ResultIsMember + })}) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls = append(calls, 2) + return check.ResultNotMember + })}) + + result := runner.Result() + require.Equal(t, check.IsMember, result.Membership) + assert.Equal(t, []int{0, 1}, calls) + }) + } + }) + + t.Run("Result on empty group uses mode default", func(t *testing.T) { + t.Parallel() + + unionRunner := newStepGroup(t.Context(), 1, unionMode, NewExecutor(nil)) + assert.Equal(t, check.ResultNotMember, unionRunner.Result()) + + intersectionRunner := newStepGroup(t.Context(), 1, intersectionMode, NewExecutor(nil)) + assert.Equal(t, check.ResultIsMember, intersectionRunner.Result()) + }) + + t.Run("Result called twice returns the same value", func(t *testing.T) { + t.Parallel() + + t.Run("decisive result", func(t *testing.T) { + t.Parallel() + + runner := newStepGroup(t.Context(), 1, unionMode, NewExecutor(nil)) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultIsMember + })}) + assert.Equal(t, check.IsMember, runner.Result().Membership) + assert.Equal(t, check.IsMember, runner.Result().Membership) + }) + + t.Run("no decisive result", func(t *testing.T) { + t.Parallel() + + // When g.set is false after all steps complete, Result() commits the final value. + // A second call must not return context.Canceled due to the internal g.cancel() from the first call. + runner := newStepGroup(t.Context(), 1, intersectionMode, NewExecutor(nil)) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultIsMember + })}) + assert.Equal(t, check.IsMember, runner.Result().Membership) + assert.Equal(t, check.IsMember, runner.Result().Membership) + }) + + t.Run("cancelled context", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + runner := newStepGroup(ctx, 1, unionMode, NewExecutor(nil)) + assert.ErrorIs(t, runner.Result().Err, context.Canceled) + assert.ErrorIs(t, runner.Result().Err, context.Canceled) + }) + }) +} + +func TestGroupConcurrent(t *testing.T) { + t.Parallel() + + t.Run("union", func(t *testing.T) { + t.Parallel() + + t.Run("siblings gets context cancellation when one sibling returns a decisive result", func(t *testing.T) { + t.Parallel() + + var ran [3]atomic.Bool + var blockedStepCancelled atomic.Bool + + runner := newStepGroup(t.Context(), 2, unionMode, NewExecutor(nil)) + + runner.Add(check.PlannedStep{Step: stepFunc(func(ctx context.Context) check.Result { + ran[0].Store(true) + <-ctx.Done() + blockedStepCancelled.Store(true) + return check.ResultNotMember + })}) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + ran[1].Store(true) + return check.ResultIsMember + })}) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + ran[2].Store(true) + return check.ResultNotMember + })}) + + result := runner.Result() + + require.Equal(t, check.IsMember, result.Membership) + assert.True(t, ran[0].Load()) + assert.True(t, ran[1].Load()) + assert.True(t, blockedStepCancelled.Load()) + assert.False(t, ran[2].Load()) + }) + + t.Run("internal sibling cancellation does not leak as error when decisive result exists", func(t *testing.T) { + t.Parallel() + + runner := newStepGroup(t.Context(), 2, unionMode, NewExecutor(nil)) + + // Step 1 propagates its context error on cancellation. + runner.Add(check.PlannedStep{Step: stepFunc(func(ctx context.Context) check.Result { + <-ctx.Done() + return check.Result{Err: ctx.Err()} + })}) + // Step 2 returns a decisive result, which internally cancels step 1. + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultIsMember + })}) + + result := runner.Result() + assert.Equal(t, check.IsMember, result.Membership) + assert.NoError(t, result.Err) + }) + + t.Run("Result blocks until all in-flight goroutines finish", func(t *testing.T) { + t.Parallel() + + var stepCompleted atomic.Bool + + runner := newStepGroup(t.Context(), 2, unionMode, NewExecutor(nil)) + + runner.Add(check.PlannedStep{Step: stepFunc(func(ctx context.Context) check.Result { + <-ctx.Done() + stepCompleted.Store(true) + return check.ResultNotMember + })}) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultIsMember + })}) + + result := runner.Result() + require.Equal(t, check.IsMember, result.Membership) + assert.True(t, stepCompleted.Load(), "Result() returned before the in-flight goroutine finished") + }) + + t.Run("Add after cancelled context does not execute the step", func(t *testing.T) { + t.Parallel() + + var calls atomic.Int64 + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + runner := newStepGroup(ctx, 2, unionMode, NewExecutor(nil)) + + require.True(t, runner.Done()) + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + calls.Add(1) + return check.ResultIsMember + })}) + + assert.Equal(t, int64(0), calls.Load()) + }) + + t.Run("Result after cancelled context returns MembershipUnknown and context.Canceled", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + runner := newStepGroup(ctx, 2, unionMode, NewExecutor(nil)) + result := runner.Result() + assert.Equal(t, check.MembershipUnknown, result.Membership) + assert.ErrorIs(t, result.Err, context.Canceled) + }) + + t.Run("context cancelled mid-step: error propagates to Result", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + + runner := newStepGroup(ctx, 2, unionMode, NewExecutor(nil)) + + // Step 1 blocks until the context is cancelled, then returns the cancellation error. + runner.Add(check.PlannedStep{Step: stepFunc(func(ctx context.Context) check.Result { + <-ctx.Done() + return check.Result{Err: ctx.Err()} + })}) + cancel() + + // Step 2 is a no-op: g.ctx is already cancelled so Add returns immediately without running it. + runner.Add(check.PlannedStep{Step: stepFunc(func(ctx context.Context) check.Result { + return check.ResultNotMember + })}) + + result := runner.Result() + assert.ErrorIs(t, result.Err, context.Canceled) + assert.Equal(t, check.MembershipUnknown, result.Membership) + }) + + t.Run("returns NotMember when no step is decisive", func(t *testing.T) { + t.Parallel() + + runner := newStepGroup(t.Context(), 3, unionMode, NewExecutor(nil)) + for range 3 { + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultNotMember + })}) + } + + result := runner.Result() + assert.Equal(t, check.NotMember, result.Membership) + assert.NoError(t, result.Err) + }) + }) + + t.Run("intersection", func(t *testing.T) { + t.Parallel() + + t.Run("all steps IsMember returns IsMember without error", func(t *testing.T) { + t.Parallel() + + // For intersection, IsMember is not decisive so g.set stays false throughout. + // Result() relies on ctxErr being nil (no internal g.cancel() fires) to reach NoDecisionResult(). + // This test documents that the success path produces no spurious error. + runner := newStepGroup(t.Context(), 3, intersectionMode, NewExecutor(nil)) + for range 3 { + runner.Add(check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + return check.ResultIsMember + })}) + } + + result := runner.Result() + assert.Equal(t, check.IsMember, result.Membership) + assert.NoError(t, result.Err) + }) + }) +} + +// plannedSteps is a test helper that creates a slice of PlannedStep from a slice of Results. +// Each step records its index in calls when executed, allowing tests to verify execution order and count. +func plannedSteps(results []check.Result, calls *[]int) []check.PlannedStep { + var mu sync.Mutex + steps := make([]check.PlannedStep, 0, len(results)) + for i, result := range results { + steps = append(steps, check.PlannedStep{Step: stepFunc(func(_ context.Context) check.Result { + mu.Lock() + *calls = append(*calls, i) + mu.Unlock() + return result + })}) + } + return steps +} + +// stepFunc is a Step whose Execute calls f directly, ignoring req and executor. +type stepFunc func(ctx context.Context) check.Result + +func (stepFunc) Kind() check.StepKind { return check.StepIsAllowed } +func (f stepFunc) Execute(ctx context.Context, _ check.CheckRequest, _ check.Executor) check.Result { + return f(ctx) +} diff --git a/internal/check/step/expand.go b/internal/check/step/expand.go new file mode 100644 index 0000000..8366a49 --- /dev/null +++ b/internal/check/step/expand.go @@ -0,0 +1,100 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/herodot" + "github.com/pkg/errors" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x/graph" +) + +// ExpandSubjectStep expands subject-set pointers under the tuple's relation and recursively checks each one. +type ExpandSubjectStep struct { + // SubjectSetTypes are the OPL-declared subject-set types for this relation. + // When empty, all subject-set pointers are expanded. + // When non-empty, these types can be used to narrow down the expansion. + SubjectSetTypes []relationtuple.SubjectSetType +} + +func (ExpandSubjectStep) Kind() check.StepKind { return check.StepExpand } + +func (s ExpandSubjectStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Trace("check expand subject") + + results, err := ex.Deps().RelationTupleManager().TraverseSubjectSetExpansion(ctx, req.Tuple, s.SubjectSetTypes) + if errors.Is(err, herodot.ErrNotFound()) { + return check.ResultNotMember + } else if err != nil { + return check.Result{Err: errors.WithStack(err)} + } + + check.ReportTuplesLoaded(ctx, len(results)) + + // Check whether the current hop already resolved the query. + for _, result := range results { + if result.Found { + check.ReportSubjectFound(ctx, result.To) + return check.ResultIsMember + } + } + + if len(results) == 0 { + return check.ResultNotMember + } + + // If we have results, but no direct hits, we need to check the next level of expansion. + // We check the depth here to prevent planning a large number of child checks when they will already be at max depth. + if req.RestDepth <= 1 { + return maxDepthReached(ex, req) + } + + // Build the visited-tracking context from the current ctx so that child + // checks share the same visited set and prevent infinite loops. + var visited bool + visitCtx := graph.InitVisited(ctx) + + runner := ex.NewUnionRunner(visitCtx) + if maxWidth := ex.Deps().Config(ctx).MaxReadWidth(); len(results) > maxWidth { + ex.Deps().Logger(). + WithField("method", "ExpandSubjectStep"). + WithField("request", req.Tuple.String()). + WithField("max_width", maxWidth). + WithField("results", len(results)). + Debug("too many results, truncating to width limit") + results = results[:maxWidth] + + runner.AddLimitation(check.LimitationMaxWidthExceeded) + } + + for _, result := range results { + if runner.Done() { + break + } + sub := &relationtuple.SubjectSet{ + Namespace: result.To.Namespace, + Object: result.To.Object, + Relation: result.To.Relation, + } + visitCtx, visited = graph.CheckAndAddVisited(visitCtx, sub) + if visited { + continue + } + runner.Add(check.PlannedStep{ + Step: IsAllowedStep{skipDirect: true}, + Req: check.CheckRequest{Tuple: result.To, RestDepth: req.RestDepth - 1}, + }) + } + return runner.Result() +} diff --git a/internal/check/step/expand_test.go b/internal/check/step/expand_test.go new file mode 100644 index 0000000..10b0e8a --- /dev/null +++ b/internal/check/step/expand_test.go @@ -0,0 +1,479 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/trace" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/testhelpers" +) + +// TestExpandSubjectStep_MemberFound verifies that ExpandSubjectStep produces +// the correct short-circuit trace when the subject is found via subject-set expansion. +func TestExpandSubjectStep_MemberFound(t *testing.T) { + t.Parallel() + + namespaceOnlyOPL := `class User implements Namespace{} class Group implements Namespace{} class File implements Namespace{}` + + tests := []struct { + scenario testhelpers.Scenario + checkInput string + expectedMembership check.Membership + expectedTrace func(t testing.TB) *trace.Node + }{ + { + scenario: testhelpers.Scenario{ + Name: "1-hop subject-set hit resolves without recursive sub-check", + Opl: namespaceOnlyOPL, + InputTuples: []string{"File:f1#viewers@Group:g1#members", "Group:g1#members@User:Alice"}, + }, + checkInput: "File:f1#viewers@User:Alice", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", + trace.DirectNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice"), + trace.ExpandNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", 1, + trace.FoundNode(t, "Group:g1#members@User:Alice"), + ), + ) + }, + }, + { + scenario: testhelpers.Scenario{ + Name: "2-hop subject-set hit resolves without recursive sub-check", + Opl: namespaceOnlyOPL, + InputTuples: []string{ + "File:f1#viewers@Group:g1#members", + "Group:g1#members@Group:g2#members", + "Group:g2#members@User:Alice", + }, + }, + checkInput: "File:f1#viewers@User:Alice", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", + trace.DirectNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice"), + trace.ExpandNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", 1, + trace.CheckNode(t, trace.ResultMember, "Group:g1#members@User:Alice", + trace.ExpandNode(t, trace.ResultMember, "Group:g1#members@User:Alice", 1, + trace.FoundNode(t, "Group:g2#members@User:Alice"), + ), + ), + ), + ) + }, + }, + } + + for _, tt := range tests { + tt.scenario.Run(t, func(t *testing.T, reg driver.Registry) { + t.Parallel() + + e := trace.NewEngine(reg) + res, tree := e.CheckRelationTupleWithTrace(t.Context(), testhelpers.TupleFromString(t, tt.checkInput), 100) + require.Equal(t, tt.expectedMembership, res.Membership) + require.NoError(t, res.Err) + require.Equal(t, trace.SortNode(tt.expectedTrace(t)).String(), trace.SortNode(trace.StripTiming(tree)).String()) + }) + } +} + +// TestExpandSubjectStep_NotMember documents the not-member traces across the three +// OPL modes (namespace-only; typed OPL; typed OPL with strict mode). +// +// Without typed OPL, the engine follows every subject-set pointer in the DB, including +// leaf subjects stored as subject-sets with an empty relation. With typed OPL the engine +// filters expansion to only the subject-set types declared in the schema, skipping leaf +// subjects that are not valid subject-set types for the relation. +func TestExpandSubjectStep_NotMember(t *testing.T) { + t.Parallel() + + typedOPL := ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: (User|SubjectSet)[] + } + } + class File implements Namespace{ + related: { + viewers: SubjectSet[] + } + } + ` + inputTuples := []string{ + "File:f1#viewers@Group:g1#members", + "Group:g1#members@User:Alice", + } + + tests := []struct { + scenario testhelpers.Scenario + checkInput string + expectedMembership check.Membership + expectedTrace func(t testing.TB) *trace.Node + }{ + { + // The engine follows every subject-set pointer including leaf subjects stored as SubjectSets. + // In only-namespaces setup, everything in DB is treated as a valid. + scenario: testhelpers.Scenario{ + Name: "namespace-only-OPL: no filter available, recurses into leaf subject-sets", + Opl: `class User implements Namespace{} class Group implements Namespace{} class File implements Namespace{}`, + InputTuples: inputTuples, + }, + checkInput: "File:f1#viewers@User:Bob", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", + trace.DirectNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob"), + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", 1, + trace.CheckNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", 1, + // Redundant: the engine doesn't know Alice is a leaf subject, + // so it creates a sub-check that always returns not_member. + trace.CheckNode(t, trace.ResultNotMember, "User:Alice#@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "User:Alice#@User:Bob", 0), + ), + ), + ), + ), + ) + }, + }, + { + // In non-strict mode the OPL filter is not applied, so the engine recurses + // into every subject-set pointer including leaf Users stored as empty-relation + // SubjectSets. This matches the namespace-only behavior. + scenario: testhelpers.Scenario{ + Name: "valid-OPL: without filter, recurses into leaf subject-sets", + Opl: typedOPL, + InputTuples: inputTuples, + }, + checkInput: "File:f1#viewers@User:Bob", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", + trace.DirectNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob"), + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", 1, + trace.CheckNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", 1, + trace.CheckNode(t, trace.ResultNotMember, "User:Alice#@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "User:Alice#@User:Bob", 0), + ), + ), + ), + ), + ) + }, + }, + { + scenario: testhelpers.Scenario{ + Name: "valid-OPL-strict: with filter, skips leaf subject-sets", + Strict: true, + Opl: typedOPL, + InputTuples: inputTuples, + }, + checkInput: "File:f1#viewers@User:Bob", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + // In strict mode, File#viewers only accepts SubjectSet. + // User:Bob# (namespace=User) is not a declared direct type for "viewers" relation, + // so the direct check is skipped entirely. Only the subject-set expansion runs. + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Bob", 1, + trace.CheckNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", + trace.ExpandNode(t, trace.ResultNotMember, "Group:g1#members@User:Bob", 0), + ), + ), + ) + }, + }, + } + + for _, tt := range tests { + tt.scenario.Run(t, func(t *testing.T, reg driver.Registry) { + t.Parallel() + + e := trace.NewEngine(reg) + res, tree := e.CheckRelationTupleWithTrace(t.Context(), testhelpers.TupleFromString(t, tt.checkInput), 100) + require.Equal(t, tt.expectedMembership, res.Membership) + require.NoError(t, res.Err) + require.Equal(t, trace.SortNode(tt.expectedTrace(t)).String(), trace.SortNode(trace.StripTiming(tree)).String()) + }) + } +} + +// TestExpandSubjectStep_StaleSubjectSet shows how stale subject-set tuples behave +// differently in strict vs non-strict mode. +// +// Scenario: OPL declares viewers: SubjectSet[], but the DB still +// contains a tuple for the old "members" relation. In strict mode the OPL filter +// blocks the stale pointer and access is denied. In non-strict mode no filter is +// applied, so the stale pointer is followed and access is incorrectly granted. +// Clients should migrate to strict mode to prevent stale tuples from granting access. +func TestExpandSubjectStep_StaleSubjectSet(t *testing.T) { + t.Parallel() + + opl := ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + admins: User[] + } + } + class File implements Namespace{ + related: { + viewers: SubjectSet[] + } + } + ` + inputTuples := []string{ + // "members" was removed from OPL but the tuple was not cleaned up in the DB. + "File:f1#viewers@Group:g1#members", + "Group:g1#members@User:Alice", + } + + tests := []struct { + scenario testhelpers.Scenario + expectedMembership check.Membership + expectedTrace func(t testing.TB) *trace.Node + }{ + { + // Without the OPL filter, the stale "members" pointer is followed and + // Alice is found as a direct member of Group:g1#members. + scenario: testhelpers.Scenario{ + Name: "valid-OPL: stale subject-set tuples are followed without OPL filter", + Opl: opl, + InputTuples: inputTuples, + }, + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", + trace.DirectNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice"), + trace.ExpandNode(t, trace.ResultMember, "File:f1#viewers@User:Alice", 1, + trace.FoundNode(t, "Group:g1#members@User:Alice"), + ), + ) + }, + }, + { + // In strict mode the OPL filter is applied: only SubjectSet + // pointers are expanded. The stale "members" tuple is filtered out. + // The direct step is also skipped because File#viewers does not declare + // User as a direct type — only SubjectSet is allowed. + scenario: testhelpers.Scenario{ + Name: "valid-OPL-strict: stale subject-set tuples are filtered by OPL", + Strict: true, + Opl: opl, + InputTuples: inputTuples, + }, + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice", + // tuples_loaded=0: expand found no valid subject-set tuples with "admins" relation. + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice", 0), + ) + }, + }, + } + + for _, tt := range tests { + tt.scenario.Run(t, func(t *testing.T, reg driver.Registry) { + t.Parallel() + + e := trace.NewEngine(reg) + res, tree := e.CheckRelationTupleWithTrace(t.Context(), testhelpers.TupleFromString(t, "File:f1#viewers@User:Alice"), 100) + require.Equal(t, tt.expectedMembership, res.Membership) + require.NoError(t, res.Err) + require.Equal(t, trace.SortNode(tt.expectedTrace(t)).String(), trace.SortNode(trace.StripTiming(tree)).String()) + }) + } +} + +func TestSubjectSetExpansionOPLFilter(t *testing.T) { + t.Parallel() + + tests := []struct { + scenario testhelpers.Scenario + checks []struct { + name string + input string + expectedMembership check.Membership + expectedTrace func(t testing.TB) *trace.Node + } + }{ + { + scenario: testhelpers.Scenario{ + Name: "nested SubjectSet chain with Direct User at the very end", + Strict: true, + Opl: ` + class User implements Namespace{} + + class Subgroup implements Namespace{ + related: { + members: User[] + } + } + + class Group implements Namespace{ + related: { + members: SubjectSet[] + } + } + class File implements Namespace{ + related: { + viewers: SubjectSet[] + } + } + `, + // According to OPL, the only valid viewer is User:Bob, as it follows + // the subjectset chain declared in OPL. + InputTuples: []string{ + // tuple exists even though "viewers" cannot accept direct-User by OPL + "File:f1#viewers@User:Alice", + + // File:f1#viewers c Group:g1#members c Subgroup:sg1#members c User:Bob + "File:f1#viewers@Group:g1#members", + "Group:g1#members@Subgroup:sg1#members", + "Subgroup:sg1#members@User:Bob", + + // Group:g1#members c User:Eve + "Group:g1#members@User:Eve", + + // SubjectID: File:f2#viewers c Group:g2#members c Alice + "File:f2#viewers@Group:g2#members", + "Group:g2#members@Alice", + }, + }, + checks: []struct { + name string + input string + expectedMembership check.Membership + expectedTrace func(t testing.TB) *trace.Node + }{ + { + name: "User:Alice is not a viewer (no path through subject-set chain)", + input: "File:f1#viewers@User:Alice", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice", + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Alice", 1, + trace.CheckNode(t, trace.ResultNotMember, "Group:g1#members@User:Alice", + trace.ExpandNode(t, trace.ResultNotMember, "Group:g1#members@User:Alice", 1, + trace.CheckNode(t, trace.ResultNotMember, "Subgroup:sg1#members@User:Alice"), + ), + ), + ), + ) + }, + }, + { + // Eve is stored as a direct member of Group:g1#members, but OPL declares + // Group#members as SubjectSet only — no direct subjects. + name: "User:Eve stored directly in Group but OPL forbids direct subjects there", + input: "File:f1#viewers@User:Eve", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "File:f1#viewers@User:Eve", + trace.ExpandNode(t, trace.ResultNotMember, "File:f1#viewers@User:Eve", 1, + trace.CheckNode(t, trace.ResultNotMember, "Group:g1#members@User:Eve", + trace.ExpandNode(t, trace.ResultNotMember, "Group:g1#members@User:Eve", 1, + trace.CheckNode(t, trace.ResultNotMember, "Subgroup:sg1#members@User:Eve"), + ), + ), + ), + ) + }, + }, + { + // Subgroup#members declares User[] only. SubjectSet with a non-empty relation + // must not satisfy that direct-type check, so no steps are taken. + name: "SubjectSet pointer rejected by direct-only User[] declaration", + input: "Subgroup:sg1#members@Subgroup:sg2#members", + expectedMembership: check.NotMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultNotMember, "Subgroup:sg1#members@Subgroup:sg2#members") + }, + }, + { + name: "User:Bob is a viewer via nested subject-set chain", + input: "File:f1#viewers@User:Bob", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@User:Bob", + trace.ExpandNode(t, trace.ResultMember, "File:f1#viewers@User:Bob", 1, + trace.CheckNode(t, trace.ResultMember, "Group:g1#members@User:Bob", + trace.ExpandNode(t, trace.ResultMember, "Group:g1#members@User:Bob", 1, + trace.FoundNode(t, "Subgroup:sg1#members@User:Bob"), + ), + ), + ), + ) + }, + }, + { + name: "Group:g1#members is a viewer via direct tuple", + input: "File:f1#viewers@Group:g1#members", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@Group:g1#members", + trace.DirectNode(t, trace.ResultMember, "File:f1#viewers@Group:g1#members"), + ) + }, + }, + { + name: "Subgroup:sg1#members is a viewer via Group:g1#members pointer", + input: "File:f1#viewers@Subgroup:sg1#members", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f1#viewers@Subgroup:sg1#members", + trace.ExpandNode(t, trace.ResultMember, "File:f1#viewers@Subgroup:sg1#members", 1, + trace.FoundNode(t, "Group:g1#members@Subgroup:sg1#members"), + ), + ) + }, + }, + { + // SubjectID has no namespace, so OPL type constraints cannot apply to it + name: "SubjectID Alice bypasses OPL type constraints and is member", + input: "File:f2#viewers@Alice", + expectedMembership: check.IsMember, + expectedTrace: func(t testing.TB) *trace.Node { + return trace.CheckNode(t, trace.ResultMember, "File:f2#viewers@Alice", + // in non-strict mode, the engine would follow the direct SubjectID pointer here and + // incorrectly find Alice as a member of Group:g1#members, even though OPL only allows + // SubjectSet. + // We don't expect SubjectID to be used in strict mode, but if it is, it should be allowed to bypass + // OPL for backward compatibility. + trace.DirectNode(t, trace.ResultNotMember, "File:f2#viewers@Alice"), + trace.ExpandNode(t, trace.ResultMember, "File:f2#viewers@Alice", 1, + trace.FoundNode(t, "Group:g2#members@Alice"), + ), + ) + }, + }, + }, + }, + } + + for _, tt := range tests { + tt.scenario.Run(t, func(t *testing.T, reg driver.Registry) { + for _, c := range tt.checks { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + + e := trace.NewEngine(reg) + res, tree := e.CheckRelationTupleWithTrace(t.Context(), testhelpers.TupleFromString(t, c.input), 100) + require.Equal(t, c.expectedMembership, res.Membership) + require.NoError(t, res.Err) + require.Equal(t, trace.SortNode(c.expectedTrace(t)).String(), trace.SortNode(trace.StripTiming(tree)).String()) + }) + } + }) + } +} diff --git a/internal/check/step/helpers.go b/internal/check/step/helpers.go new file mode 100644 index 0000000..f73231c --- /dev/null +++ b/internal/check/step/helpers.go @@ -0,0 +1,78 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/relationtuple" +) + +// astRelationFor looks up the AST Relation for the given tuple's namespace and +// relation. Returns nil (not an error) when no namespace config is present. +func astRelationFor(ctx context.Context, nm namespace.Manager, r *relationTuple) (*ast.Relation, error) { + return namespace.ASTRelationFor(ctx, nm, r.Namespace, r.Relation) +} + +// maxDepthReached logs a debug message and returns MembershipUnknown. +// Call it when RestDepth is exhausted and the check cannot be expanded further. +func maxDepthReached(ex check.Executor, req check.CheckRequest) check.Result { + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Debug("reached max-depth, therefore this query will not be further expanded") + + return check.Result{Membership: check.MembershipUnknown, Limitation: check.LimitationMaxDepthExceeded} +} + +// subjectSetTypesFor returns the OPL-declared subject-set types for the relation. +// It looks up the AST for each next-level (namespace, relation) to determine +// whether the Subject can be direct-match there. +func subjectSetTypesFor(ctx context.Context, nm namespace.Manager, subject relationtuple.Subject, relation *ast.Relation) ([]relationtuple.SubjectSetType, error) { + if relation == nil { + return nil, nil + } + var types []relationtuple.SubjectSetType + for _, t := range relation.Types { + // empty relation means direct membership, so not a SubjectSet<> declaration + if t.Relation == "" { + continue + } + nextRel, err := namespace.ASTRelationFor(ctx, nm, t.Namespace, t.Relation) + if err != nil { + return nil, err + } + types = append(types, relationtuple.SubjectSetType{ + Namespace: t.Namespace, + Relation: t.Relation, + AllowsDirect: AllowsDirectMember(nextRel, subject), + }) + } + return types, nil +} + +// AllowsDirectMember reports whether OPL allows a direct subject match for the given relation. +// A relation with a SubjectSetRewrite is computed, so no direct tuple conforms to OPL, +// regardless of the subject kind. +// SubjectID subjects have no namespace, so declared types cannot constrain them and true +// is returned conservatively. +// For SubjectSet subjects, both namespace and relation must match a declared type exactly. +func AllowsDirectMember(relation *ast.Relation, subject relationtuple.Subject) bool { + s, isSubjectSet := subject.(*relationtuple.SubjectSet) + if !isSubjectSet { + // For SubjectID, allow direct when the relation is not computed via a rewrite. + return relation == nil || relation.SubjectSetRewrite == nil + } + if relation == nil || relation.SubjectSetRewrite != nil { + return false + } + for _, t := range relation.Types { + if t.Namespace == s.Namespace && t.Relation == s.Relation { + return true + } + } + return false +} diff --git a/internal/check/step/helpers_test.go b/internal/check/step/helpers_test.go new file mode 100644 index 0000000..d8c075d --- /dev/null +++ b/internal/check/step/helpers_test.go @@ -0,0 +1,107 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/keto/internal/check/step" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/testhelpers" +) + +func TestAllowsDirectMember(t *testing.T) { + t.Parallel() + + var ( + rewriteRel = &ast.Relation{SubjectSetRewrite: &ast.SubjectSetRewrite{}} + directRel = &ast.Relation{Name: "users", Types: []ast.RelationType{{Namespace: "User"}}} // users: User[] + anotherRel = &ast.Relation{Name: "keys", Types: []ast.RelationType{{Namespace: "ApiKey"}}} // keys: ApiKey[] + subjectSetRel = &ast.Relation{Name: "members", Types: []ast.RelationType{{Namespace: "Group", Relation: "members"}}} // members: SubjectSet + ) + + tests := []struct { + name string + relation *ast.Relation + subjectStr string + want bool + }{ + { + name: "SubjectID always returns true (conservative)", + relation: directRel, + subjectStr: "fixedID", + want: true, + }, + { + name: "SubjectID with nil relation returns true (conservative)", + relation: nil, + subjectStr: "fixedID", + want: true, + }, + { + name: "SubjectID with SubjectSetRewrite returns false (computed relation has no direct tuples)", + relation: rewriteRel, + subjectStr: "fixedID", + want: false, + }, + { + name: "SubjectSet with nil relation returns false", + relation: nil, + subjectStr: "User:u1", + want: false, + }, + { + name: "SubjectSet with SubjectSetRewrite returns false", + relation: rewriteRel, + subjectStr: "User:u1", + want: false, + }, + { + name: "SubjectSet namespace matches direct type", + relation: directRel, + subjectStr: "User:u1", + want: true, + }, + { + name: "subject namespace does not match any direct type", + relation: anotherRel, + subjectStr: "User:u1", + want: false, + }, + { + name: "relation doesn't even have any User type", + relation: subjectSetRel, + subjectStr: "User:u1", + want: false, + }, + { + name: "subject is a single object, but relation is a SubjectSet, so no direct match allowed", + relation: subjectSetRel, + subjectStr: "Group:g1", + want: false, + }, + { + name: "subject is group#member, and relation is also SubjectSet, so direct match is allowed", + relation: subjectSetRel, + subjectStr: "Group:g1#members", + want: true, + }, + { + name: "subject is group#member, but relation is a Direct", + relation: &ast.Relation{Name: "groups", Types: []ast.RelationType{{Namespace: "Group"}}}, + subjectStr: "Group:g1#members", + want: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + subject := testhelpers.SubjectFromString(t, tc.subjectStr) + assert.Equal(t, tc.want, step.AllowsDirectMember(tc.relation, subject)) + }) + } +} diff --git a/internal/check/step/invert.go b/internal/check/step/invert.go new file mode 100644 index 0000000..7feda58 --- /dev/null +++ b/internal/check/step/invert.go @@ -0,0 +1,40 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/namespace/ast" +) + +// InvertStep evaluates its child and inverts the membership result. +// IsMember becomes NotMember and vice versa; Unknown and Error pass through. +type InvertStep struct { + Child ast.Child +} + +func (InvertStep) Kind() check.StepKind { return check.StepInvert } + +func (s InvertStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Trace("invert check") + + childStep, err := rewriteChildToStep(s.Child) + if err != nil { + return check.Result{Err: err} + } + + result := ex.Run(ctx, childStep, req) + + switch result.Membership { + case check.IsMember: + result.Membership = check.NotMember + case check.NotMember: + result.Membership = check.IsMember + } + return result +} diff --git a/internal/check/step/rewrite.go b/internal/check/step/rewrite.go new file mode 100644 index 0000000..2db2ad6 --- /dev/null +++ b/internal/check/step/rewrite.go @@ -0,0 +1,148 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/pkg/errors" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/relationtuple" +) + +// RewriteStep evaluates a SubjectSetRewrite node (OR or AND). +// It translates the rewrite's children into PlannedSteps via planRewrite and +// then delegates to RunUnion (OR) or RunIntersection (AND). +type RewriteStep struct { + Rewrite *ast.SubjectSetRewrite +} + +func (RewriteStep) Kind() check.StepKind { return check.StepRewrite } + +func (s RewriteStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) check.Result { + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + Trace("check subject-set rewrite") + + steps, err := planRewrite(ctx, ex, req, s.Rewrite) + if err != nil { + return check.Result{Err: err} + } + + if s.Rewrite.Operation == ast.OperatorAnd { + return ex.RunIntersection(ctx, steps...) + } + return ex.RunUnion(ctx, steps...) +} + +// planRewrite translates a SubjectSetRewrite AST node into a flat list of +// PlannedSteps ready for execution. It applies the OR-shortcut optimization: +// ComputedSubjectSet children in an OR are batched into a single +// DirectMultiStep followed by individual IsAllowedStep calls. +func planRewrite(ctx context.Context, ex check.Executor, req check.CheckRequest, rewrite *ast.SubjectSetRewrite) ([]check.PlannedStep, error) { + var steps []check.PlannedStep + handled := make(map[int]struct{}, len(rewrite.Children)) + + // OR shortcut: batch all ComputedSubjectSet children into one DirectMulti + // check plus individual IsAllowed calls (matching the current engine's + // OR-of-computed-usersets optimization). + if rewrite.Operation == ast.OperatorOr { + var computedRelations []string + for i, child := range rewrite.Children { + if c, ok := child.(*ast.ComputedSubjectSet); ok { + handled[i] = struct{}{} + computedRelations = append(computedRelations, c.Relation) + } + } + + if len(computedRelations) > 0 { + namespaceManager, err := ex.Deps().Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + // In strict mode, skip direct checks for relations where OPL does not + // allow a direct subject match, mirroring the DirectStep gate in + // IsAllowedStep so nested and top-level checks agree. + var directRelations []string + isStrictMode := ex.Deps().Config(ctx).StrictMode() + for _, rel := range computedRelations { + astRel, _ := namespace.ASTRelationFor(ctx, namespaceManager, req.Tuple.Namespace, rel) + if isStrictMode && !AllowsDirectMember(astRel, req.Tuple.Subject) { + continue + } + directRelations = append(directRelations, rel) + } + + if len(directRelations) > 0 { + steps = append(steps, check.PlannedStep{ + Step: DirectMultiStep{Relations: directRelations}, + Req: req, + }) + } + + // Each computed relation also gets a full IsAllowed check + // (skipDirect=true since DirectMulti already covers direct hits). + for _, rel := range computedRelations { + steps = append(steps, check.PlannedStep{ + Step: IsAllowedStep{skipDirect: true}, + Req: check.CheckRequest{ + Tuple: &relationtuple.RelationTuple{ + Namespace: req.Tuple.Namespace, + Object: req.Tuple.Object, + Relation: rel, + Subject: req.Tuple.Subject, + }, + RestDepth: req.RestDepth - 1, + }, + }) + } + } + } + + // Process remaining (non-shortcut) children. + for i, child := range rewrite.Children { + if _, ok := handled[i]; ok { + continue + } + step, err := rewriteChildToStep(child) + if err != nil { + return nil, err + } + + childReq := req + switch child.(type) { + case *ast.SubjectSetRewrite: + childReq = req.WithDepth(req.RestDepth - 1) + } + + steps = append(steps, check.PlannedStep{Step: step, Req: childReq}) + } + + return steps, nil +} + +// rewriteChildToStep converts a single rewrite AST child node into an +// executable Step. Returns an error for unrecognised node types. +func rewriteChildToStep(child ast.Child) (check.Step, error) { + switch c := child.(type) { + case *ast.TupleToSubjectSet: + return TraverseStep{TTU: c}, nil + case *ast.ComputedSubjectSet: + return ComputedUsersetStep{Relation: c.Relation}, nil + case *ast.SubjectSetRewrite: + return RewriteStep{Rewrite: c}, nil + case *ast.InvertResult: + return InvertStep{Child: c.Child}, nil + default: + return nil, errors.New("not implemented: unknown rewrite child type") + } +} diff --git a/internal/check/step/testmain_test.go b/internal/check/step/testmain_test.go new file mode 100644 index 0000000..b2f4df4 --- /dev/null +++ b/internal/check/step/testmain_test.go @@ -0,0 +1,17 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/dgraph-io/ristretto/v2.(*defaultPolicy[...]).processItems"), + goleak.IgnoreTopFunction("github.com/dgraph-io/ristretto/v2.(*Cache[...]).processItems"), + ) +} diff --git a/internal/check/step/traverse.go b/internal/check/step/traverse.go new file mode 100644 index 0000000..4bbe5db --- /dev/null +++ b/internal/check/step/traverse.go @@ -0,0 +1,84 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package step + +import ( + "context" + + "github.com/ory/x/otelx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/relationtuple" +) + +// TraverseStep implements the tuple-to-subject-set (TTU) rewrite. +// It loads all tuples matching the TTU relation on the request object, then +// for each subject-set subject checks membership under the computed relation. +type TraverseStep struct { + TTU *ast.TupleToSubjectSet +} + +func (TraverseStep) Kind() check.StepKind { return check.StepTraverse } + +func (s TraverseStep) Execute(ctx context.Context, req check.CheckRequest, ex check.Executor) (res check.Result) { + ctx, span := ex.Deps().Tracer(ctx).Tracer().Start(ctx, "check.step.TraverseStep") + var rowCount int + defer func() { + span.SetAttributes(attribute.Int("tuples_loaded", rowCount)) + otelx.End(span, &res.Err) + }() + + if req.RestDepth <= 0 { + return maxDepthReached(ex, req) + } + + ex.Deps().Logger(). + WithField("request", req.Tuple.String()). + WithField("tuple to subject-set relation", s.TTU.Relation). + WithField("tuple to subject-set computed", s.TTU.ComputedSubjectSetRelation). + Trace("check tuple to subjectSet") + + runner := ex.NewUnionRunner(ctx) + for nextPage, _ := keysetpagination.NewPaginator(); !nextPage.IsLast() && !runner.Done(); { + var tuples []*relationtuple.RelationTuple + var err error + tuples, nextPage, err = ex.Deps().RelationTupleManager().GetRelationTuples(runner.Ctx(), &query{ + Namespace: &req.Tuple.Namespace, + Object: &req.Tuple.Object, + Relation: &s.TTU.Relation, + }, nextPage.ToOptions()...) + if err != nil { + return check.Result{Err: errors.WithStack(err)} + } + rowCount += len(tuples) + check.ReportTuplesLoaded(runner.Ctx(), len(tuples)) + + for _, t := range tuples { + subSet, ok := t.Subject.(*relationtuple.SubjectSet) + if !ok { + continue + } + runner.Add(check.PlannedStep{ + Step: IsAllowedStep{}, + Req: check.CheckRequest{ + Tuple: &relationtuple.RelationTuple{ + Namespace: subSet.Namespace, + Object: subSet.Object, + Relation: s.TTU.ComputedSubjectSetRelation, + Subject: req.Tuple.Subject, + }, + RestDepth: req.RestDepth - 1, + }, + }) + if runner.Done() { + break + } + } + } + return runner.Result() +} diff --git a/internal/check/testfixtures/project_opl.ts b/internal/check/testfixtures/project_opl.ts new file mode 100644 index 0000000..6a8d546 --- /dev/null +++ b/internal/check/testfixtures/project_opl.ts @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +import {Namespace, Context} from '@ory/keto-namespace-types' + +class User implements Namespace {} + +class Project implements Namespace { + related: { + owner: User[] + developer: User[] + } + + permits = { + isOwner: (ctx: Context) => this.related.owner.includes(ctx.subject), + isOwnerOrDeveloper: (ctx: Context) => + this.related.owner.includes(ctx.subject) || + this.related.developer.includes(ctx.subject), + writeCollaborator: (ctx: Context) => + this.permits.isOwner(ctx), + readCollaborator: (ctx: Context) => + this.permits.isOwnerOrDeveloper(ctx), + deleteProject: (ctx: Context) => this.permits.isOwner(ctx), + writeProject: (ctx: Context) => + this.permits.isOwnerOrDeveloper(ctx), + readProject: (ctx: Context) => + this.permits.isOwnerOrDeveloper(ctx), + } +} diff --git a/internal/check/testmain_test.go b/internal/check/testmain_test.go new file mode 100644 index 0000000..ecf2dc8 --- /dev/null +++ b/internal/check/testmain_test.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check_test + +import ( + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/dgraph-io/ristretto/v2.(*defaultPolicy[...]).processItems"), + goleak.IgnoreTopFunction("github.com/dgraph-io/ristretto/v2.(*Cache[...]).processItems"), + goleak.IgnoreTopFunction("net/http.(*persistConn).readLoop"), + goleak.IgnoreTopFunction("net/http.(*persistConn).writeLoop"), + goleak.IgnoreTopFunction("database/sql.(*DB).connectionOpener"), + ) +} diff --git a/internal/check/trace/engine.go b/internal/check/trace/engine.go new file mode 100644 index 0000000..b64628f --- /dev/null +++ b/internal/check/trace/engine.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package trace + +import ( + "context" + + "github.com/ory/x/otelx" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/step" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/x/events" +) + +// Engine builds a runtime with tracing enabled and returns the debug Node +// tree alongside the check result. +type Engine struct { + deps check.EngineDependencies +} + +func NewEngine(d check.EngineDependencies) *Engine { + return &Engine{deps: d} +} + +func (e *Engine) d() check.EngineDependencies { return e.deps } + +// CheckIsMemberWithTrace is like Engine.CheckIsMember but also returns a debug +// tree that traces every sub-check performed. +func (e *Engine) CheckIsMemberWithTrace(ctx context.Context, tuple *relationtuple.RelationTuple, restDepth int) (bool, *Node, error) { + res, tree := e.CheckRelationTupleWithTrace(ctx, tuple, restDepth) + return res.Membership == check.IsMember, tree, res.Err +} + +// CheckRelationTupleWithTrace returns the check result and the debug tree. +func (e *Engine) CheckRelationTupleWithTrace(ctx context.Context, tuple *relationtuple.RelationTuple, restDepth int) (res check.Result, tree *Node) { + ctx, span := e.d().Tracer(ctx).Tracer().Start(ctx, "Engine.CheckRelationTupleWithTrace") + defer otelx.End(span, &res.Err) + + session := NewSession() + executor := step.NewExecutor(e.deps, session.Middleware(), step.CallLimitMiddleware()) + + res = executor.CheckRelationTuple(ctx, tuple, restDepth) + session.Wait() + if res.Err == nil { + span.AddEvent(events.NewPermissionsChecked(ctx)) + } + return res, session.Root() +} diff --git a/internal/check/trace/engine_test.go b/internal/check/trace/engine_test.go new file mode 100644 index 0000000..0083fca --- /dev/null +++ b/internal/check/trace/engine_test.go @@ -0,0 +1,607 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package trace_test + +import ( + "context" + "slices" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/check/trace" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/schema" +) + +// # Non-determinism constraint +// +// Expand and traverse (tuple-to-subject-set) children are ordered by +// shard_id (random UUIDv4) and require sortNode normalization. +// +// As a result, we cannot write tests where multiple Expand or Traverse siblings +// compete to produce member; which one wins first is non-deterministic, as the +// child tuples are not executed in the order we create them. +// Therefore: +// - sortNode is used to sort the children of an Expand or Traverse nodes +// - Tests where all branches return not-member are always safe: every branch +// runs to completion before not-member is returned. +// - Do not write tests where Expand or Traverse has multiple children, and one child +// returns member, the other one notMember; which one runs first is non-deterministic; + +type testcase struct { + name string + opl string + inputTuples []string + maxDepth int + + checkInput string + expected func(t testing.TB) *trace.Node + strict bool + only bool +} + +func TestTraceEngine(t *testing.T) { + t.Parallel() + + tests := []testcase{ + // ── no-OPL mode ────────────────────────────────────────────────────────── + + { + // Engine finds the tuple immediately via direct lookup. + // Expand is registered eagerly, but the Executor already has a decisive + // result from the direct match, so the expansion work is skipped. + name: "no-OPL: direct hit", + inputTuples: []string{ + "Group:eng#members@User:1", + }, + checkInput: "Group:eng#members@User:1", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, member, "Group:eng#members@User:1", + directNode(t, member, "Group:eng#members@User:1"), + ) + }, + }, + { + // Engine follows a three-hop SubjectSet chain to find a member. + name: "no-OPL: multi-hop SubjectSet expansion", + inputTuples: []string{ + "Group:a#members@User:1", + "Group:b#members@Group:a#members", + "Group:c#members@Group:b#members", + }, + checkInput: "Group:c#members@User:1", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, member, "Group:c#members@User:1", + directNode(t, notMember, "Group:c#members@User:1"), + expandNode(t, member, "Group:c#members@User:1", 1, + checkNode(t, member, "Group:b#members@User:1", + expandNode(t, member, "Group:b#members@User:1", 1, + foundNode(t, "Group:a#members@User:1"), + ), + ), + ), + ) + }, + }, + { + // Engine hits the depth cap at restDepth=0 and returns unknown. + name: "no-OPL: depth limit stops expansion", + maxDepth: 2, + inputTuples: []string{ + "Group:a#members@User:1", + "Group:b1#members@Group:a#members", + "Group:c#members@Group:b1#members", + }, + checkInput: "Group:c#members@User:99", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, unknown, "Group:c#members@User:99", + directNode(t, notMember, "Group:c#members@User:99"), + expandNode(t, unknown, "Group:c#members@User:99", 1, + checkNode(t, unknown, "Group:b1#members@User:99", + expandNode(t, unknown, "Group:b1#members@User:99", 1), + ), + ), + ) + }, + }, + + // ── strict OPL mode ─────────────────────────────────────────────────────── + + { + // In strict mode the only type-safe checks are fired; + // no useless direct/expand on relations that are "permit"; + // Subject-set fires only when OPL field declares a SubjectSet<..>; + name: "strict OPL: SubjectSet expand fires only for declared types", + strict: true, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace { + related: { + viewers: SubjectSet[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@Group:g1#members", + }, + checkInput: "File:f1#view@User:u1", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, notMember, "File:f1#view@User:u1", + unionNode(t, notMember, "File:f1#view@User:u1", + // viewers only declares SubjectSet, so User:u1 can + // never be a direct match, so no direct or multi-direct check runs. + checkNode(t, notMember, "File:f1#viewers@User:u1", + // expand fires because viewers declares SubjectSet. + // expand found 1 node, which is Group:g1#members, which is not a direct hit → not-member. + expandNode(t, notMember, "File:f1#viewers@User:u1", 1, + // skipDirect=true + no SubjectSet types on members → empty check + checkNode(t, notMember, "Group:g1#members@User:u1"), + ), + ), + ), + ) + }, + }, + { + // Strict OR with multiple computed-usersets and a TTU. + // The OR shortcut batches all plain computed-usersets into a single multiDirect check. + // Plain related fields (viewers, viewers2) with no SubjectSet types produce + // empty checks because multiDirect already covers them, and sets skipDirect=true. + name: "strict OPL: OR batches computed-usersets, suppresses spurious permit checks", + strict: true, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewers2: User[] + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || + this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)) || + this.related.viewers2.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + + "File:f1#viewers2@User:10", + "File:f1#viewers2@User:11", + + "File:f1#viewerGroups@Group:g1", + "Group:g1#members@User:4", + "Group:g1#members@User:5", + }, + checkInput: "File:f1#view@User:100", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, notMember, "File:f1#view@User:100", + unionNode(t, notMember, "File:f1#view@User:100", + // OR shortcut batches viewers + viewers2 into one multi-relation direct check. + multiDirectNode(t, notMember, "File:f1#view@User:100", []string{"viewers", "viewers2"}), + // viewers and viewers2 are plain User[] fields: empty checks in strict mode. + checkNode(t, notMember, "File:f1#viewers@User:100"), + checkNode(t, notMember, "File:f1#viewers2@User:100"), + // TTU traverses viewerGroups; Group:g1 has no User:100 → not-member. + traverseNode(t, notMember, "File:f1#viewerGroups@User:100", 1, + checkNode(t, notMember, "Group:g1#members@User:100", + directNode(t, notMember, "Group:g1#members@User:100"), + ), + ), + ), + ) + }, + }, + { + // Strict OR where the TTU branch finds a member. + name: "strict OPL: OR with TTU finds member", + strict: true, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || + this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "File:f1#viewerGroups@Group:g1", + "Group:g1#members@User:4", + }, + checkInput: "File:f1#view@User:4", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, member, "File:f1#view@User:4", + unionNode(t, member, "File:f1#view@User:4", + // multiDirect for viewers: User:4 is not a direct viewer. + multiDirectNode(t, notMember, "File:f1#view@User:4", []string{"viewers"}), + // viewers is User[]: empty check in strict+skipDirect. + checkNode(t, notMember, "File:f1#viewers@User:4"), + // TTU loads Group:g1; User:4 is a direct member → member. + // strict: members is User[] (no SubjectSet types) → no expand registered. + traverseNode(t, member, "File:f1#viewerGroups@User:4", 1, + checkNode(t, member, "Group:g1#members@User:4", + directNode(t, member, "Group:g1#members@User:4"), + ), + ), + ), + ) + }, + }, + { + // NOT inverts an inner member result to not-member. + name: "strict OPL: NOT inverts member to not-member", + strict: true, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + blocklist: User[] + } + permits = { + view: (ctx: Context) => !this.related.blocklist.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#blocklist@User:blocked", + }, + checkInput: "File:f1#view@User:blocked", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, notMember, "File:f1#view@User:blocked", + unionNode(t, notMember, "File:f1#view@User:blocked", + invertNode(t, notMember, "File:f1#view@User:blocked", + computedNode(t, member, "File:f1#blocklist@User:blocked", + checkNode(t, member, "File:f1#blocklist@User:blocked", + directNode(t, member, "File:f1#blocklist@User:blocked"), + ), + ), + ), + ), + ) + }, + }, + { + // NOT inverts an inner not-member result to member. + name: "strict OPL: NOT inverts not-member to member", + strict: true, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + blocklist: User[] + } + permits = { + view: (ctx: Context) => !this.related.blocklist.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#blocklist@User:blocked", + }, + checkInput: "File:f1#view@User:free", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, member, "File:f1#view@User:free", + unionNode(t, member, "File:f1#view@User:free", + invertNode(t, member, "File:f1#view@User:free", + computedNode(t, notMember, "File:f1#blocklist@User:free", + checkNode(t, notMember, "File:f1#blocklist@User:free", + directNode(t, notMember, "File:f1#blocklist@User:free"), + ), + ), + ), + ), + ) + }, + }, + { + name: "strict OPL: TTU with permit call inside traverse", + strict: true, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + permits = { + isMember: (ctx: Context) => this.related.members.includes(ctx.subject) + } + } + class File implements Namespace{ + related: { + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewerGroups.traverse(g => g.permits.isMember(ctx)) + } + } + `, + inputTuples: []string{ + "Group:g1#members@User:1", + "Group:g1#members@User:2", + "File:f1#viewerGroups@Group:g1", + }, + checkInput: "File:f1#view@User:99", + expected: func(t testing.TB) *trace.Node { + return checkNode(t, notMember, "File:f1#view@User:99", + unionNode(t, notMember, "File:f1#view@User:99", + traverseNode(t, notMember, "File:f1#viewerGroups@User:99", 1, + checkNode(t, notMember, "Group:g1#isMember@User:99", + unionNode(t, notMember, "Group:g1#isMember@User:99", + multiDirectNode(t, notMember, "Group:g1#members@User:99", []string{"members"}), + checkNode(t, notMember, "Group:g1#members@User:99"), + ), + ), + ), + ), + ) + }, + }, + + // ── non-strict OPL mode ─────────────────────────────────────────────────── + + { + // Non-strict mode fires the rewrite, a direct check, AND a subject-set + // expand concurrently even for a permit relation (view). + name: "non-strict OPL: OR on permit reference emits spurious direct and expand", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + viewGroup: (ctx: Context) => this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)), + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || this.permits.viewGroup(ctx) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "Group:g1#members@User:4", + "Group:g1#members@User:5", + "Group:g2#members@User:6", + "Group:g2#members@User:7", + "Group:g3#members@User:8", + "Group:g3#members@User:9", + + "File:f1#viewerGroups@Group:g1", + "File:f1#viewerGroups@Group:g2", + "File:f1#viewerGroups@Group:g3", + }, + checkInput: "File:f1#view@User:100", + expected: func(t testing.TB) *trace.Node { + leafCheck := func(s string) *trace.Node { + return checkNode(t, notMember, s, + expandNode(t, notMember, s, 0), + ) + } + groupCheck := func(g string, users ...string) *trace.Node { + members := make([]*trace.Node, len(users)) + for i, u := range users { + members[i] = leafCheck("User:" + u + "#@User:100") + } + return checkNode(t, notMember, "Group:"+g+"#members@User:100", + directNode(t, notMember, "Group:"+g+"#members@User:100"), + expandNode(t, notMember, "Group:"+g+"#members@User:100", len(users), members...), + ) + } + return checkNode(t, notMember, "File:f1#view@User:100", + unionNode(t, notMember, "File:f1#view@User:100", + multiDirectNode(t, notMember, "File:f1#view@User:100", []string{"viewers", "viewGroup"}), + checkNode(t, notMember, "File:f1#viewers@User:100", + expandNode(t, notMember, "File:f1#viewers@User:100", 3, + leafCheck("User:1#@User:100"), + leafCheck("User:2#@User:100"), + leafCheck("User:3#@User:100"), + ), + ), + checkNode(t, notMember, "File:f1#viewGroup@User:100", + unionNode(t, notMember, "File:f1#viewGroup@User:100", + traverseNode(t, notMember, "File:f1#viewerGroups@User:100", 3, + groupCheck("g1", "4", "5"), + groupCheck("g2", "6", "7"), + groupCheck("g3", "8", "9"), + ), + ), + expandNode(t, notMember, "File:f1#viewGroup@User:100", 0), + ), + ), + directNode(t, notMember, "File:f1#view@User:100"), + expandNode(t, notMember, "File:f1#view@User:100", 0), + ) + }, + }, + { + // AND: both arms must complete before the result is known, so + // the full tree is always explored regardless of intermediate results. + name: "non-strict OPL: AND requires both arms; permit-calls-permit", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + viewGroup: (ctx: Context) => this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)), + + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) && + this.permits.viewGroup(ctx) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "Group:g1#members@User:4", + "Group:g1#members@User:5", + "Group:g2#members@User:6", + "Group:g2#members@User:7", + "Group:g3#members@User:8", + "Group:g3#members@User:9", + + "File:f1#viewerGroups@Group:g1", + "File:f1#viewerGroups@Group:g2", + "File:f1#viewerGroups@Group:g3", + }, + checkInput: "File:f1#view@User:1", + expected: func(t testing.TB) *trace.Node { + leafCheck := func(s string) *trace.Node { + return checkNode(t, notMember, s, + expandNode(t, notMember, s, 0), + ) + } + groupCheck := func(g string, users ...string) *trace.Node { + members := make([]*trace.Node, len(users)) + for i, u := range users { + members[i] = leafCheck("User:" + u + "#@User:1") + } + + return checkNode(t, notMember, "Group:"+g+"#members@User:1", + directNode(t, notMember, "Group:"+g+"#members@User:1"), + expandNode(t, notMember, "Group:"+g+"#members@User:1", len(users), members...), + ) + } + return checkNode(t, notMember, "File:f1#view@User:1", + intersectionNode(t, notMember, "File:f1#view@User:1", + computedNode(t, member, "File:f1#viewers@User:1", + checkNode(t, member, "File:f1#viewers@User:1", + directNode(t, member, "File:f1#viewers@User:1"), + ), + ), + computedNode(t, notMember, "File:f1#viewGroup@User:1", + checkNode(t, notMember, "File:f1#viewGroup@User:1", + unionNode(t, notMember, "File:f1#viewGroup@User:1", + traverseNode(t, notMember, "File:f1#viewerGroups@User:1", 3, + groupCheck("g1", "4", "5"), + groupCheck("g2", "6", "7"), + groupCheck("g3", "8", "9"), + ), + ), + directNode(t, notMember, "File:f1#viewGroup@User:1"), + expandNode(t, notMember, "File:f1#viewGroup@User:1", 0), + ), + ), + ), + directNode(t, notMember, "File:f1#view@User:1"), + expandNode(t, notMember, "File:f1#view@User:1", 0), + ) + }, + }, + } + focused := slices.ContainsFunc(tests, func(tt testcase) bool { return tt.only }) + for _, tt := range tests { + if focused && !tt.only { + continue + } + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + opts := []driver.TestRegistryOption{driver.WithLogLevel("info")} + + if tt.opl != "" { + _, errs := schema.Parse(tt.opl) + require.Empty(t, errs, "invalid OPL in test case: %v", errs) + } + + if tt.opl != "" { + opts = append(opts, driver.WithOPL(tt.opl)) + } else { + opts = append(opts, driver.WithConfig(config.KeyNamespaces, []*namespace.Namespace{})) + } + + if tt.maxDepth != 0 { + opts = append(opts, driver.WithConfig(config.KeyLimitMaxReadDepth, tt.maxDepth)) + } + + if tt.strict { + opts = append(opts, driver.WithConfig(config.KeyFeatureFlagStrictMode, tt.strict)) + } + + reg := driver.NewSqliteTestRegistry(t, opts...) + e := trace.NewEngine(reg) + + ctx := context.Background() + for _, row := range tt.inputTuples { + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(ctx, testhelpers.TupleFromString(t, row))) + } + + _, tree, err := e.CheckIsMemberWithTrace(ctx, testhelpers.TupleFromString(t, tt.checkInput), 100) + require.NoError(t, err) + + expected := trace.SortNode(tt.expected(t)) + actual := trace.SortNode(trace.StripTiming(tree)) + require.Equal(t, expected.String(), actual.String()) + }) + } +} + +// result helpers — used as the first argument to every node builder. +const ( + member = trace.ResultMember + notMember = trace.ResultNotMember + unknown = trace.ResultUnknown +) + +// Local aliases for the exported helpers in trace so test cases stay concise. +var ( + checkNode = trace.CheckNode + directNode = trace.DirectNode + expandNode = trace.ExpandNode + foundNode = trace.FoundNode + unionNode = trace.UnionNode + intersectionNode = trace.IntersectionNode + invertNode = trace.InvertNode + computedNode = trace.ComputedNode + traverseNode = trace.TraverseNode + multiDirectNode = trace.MultiDirectNode +) diff --git a/internal/check/trace/node.go b/internal/check/trace/node.go new file mode 100644 index 0000000..ce67af2 --- /dev/null +++ b/internal/check/trace/node.go @@ -0,0 +1,199 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package trace + +import ( + "context" + "errors" + "strconv" + "strings" + "sync" + "time" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/relationtuple" +) + +type ( + NodeKind string + NodeResult string +) + +const ( + NodeIsAllowed NodeKind = "is_allowed" + NodeDirect NodeKind = "direct" + NodeMultiDirect NodeKind = "direct_multi" + NodeExpandSubject NodeKind = "expand_subject" + NodeFoundSubject NodeKind = "expand_found" + NodeComputed NodeKind = "computed" + NodeTraverse NodeKind = "traverse" + NodeUnion NodeKind = "union" + NodeIntersection NodeKind = "intersection" + NodeInvert NodeKind = "invert" +) + +func (k NodeKind) String() string { return string(k) } + +const ( + // ResultMember indicates the subject is a member. + ResultMember NodeResult = "member" + // ResultNotMember indicates the subject is not a member. + ResultNotMember NodeResult = "not_member" + // ResultError indicates an error occurred during the check. + ResultError NodeResult = "error" + // ResultSkipped indicates the check was registered but never executed + // because a sibling check resolved the parent before this one ran. + ResultSkipped NodeResult = "skipped" + // ResultCancelled indicates the check was cancelled mid-execution + // because a sibling check resolved. + ResultCancelled NodeResult = "cancelled" + // ResultUnknown indicates the check was cut short because restDepth + // reached zero; membership could not be determined. + ResultUnknown NodeResult = "unknown" +) + +func (r NodeResult) prunable() bool { + return r == ResultNotMember || r == ResultSkipped +} + +// MaxExpandChildren is the maximum number of non-significant children kept on +// expand_subject and traverse nodes. Children whose result is member, error, +// or cancelled are always kept regardless of this limit. +const MaxExpandChildren = 5 + +// Node is one step in the debug tree produced by a permission check. It +// records what kind of operation was performed, what the input tuple was, what +// the result was, how many database records were fetched, and any sub-steps +// that were spawned. +type Node struct { + Kind NodeKind + Tuple *relationtuple.RelationTuple + Result NodeResult + + // Relations holds the batched relation names for NodeMultiDirect nodes. + Relations []string + + // TuplesLoaded is the number of database records fetched by this node. + TuplesLoaded int + + // TruncatedChildren is the number of children that were dropped to keep + // the tree size manageable. + TruncatedChildren int + + Children []*Node + + // Duration is the wall-clock time spent in this node (including children). + Duration time.Duration + + // parent is set when this node is appended to a parent, so that + // dropIfOverLimit can remove it after the result is known. + parent *Node + + // mu protects Children, TruncatedChildren, TuplesLoaded, and prunableCount. + mu sync.Mutex + + // prunableCount counts how many children have a prunable result. + prunableCount int +} + +func (n *Node) setResult(result check.Result) { + if result.Err != nil { + n.Result = ResultError + if errors.Is(result.Err, context.Canceled) { + n.Result = ResultCancelled + } + return + } + switch result.Membership { + case check.IsMember: + n.Result = ResultMember + case check.NotMember: + n.Result = ResultNotMember + default: + n.Result = ResultUnknown + } +} + +// dropIfOverLimit removes this node from its parent when the parent is an +// expand or TTU node that already has MaxExpandChildren prunable children. +func (n *Node) dropIfOverLimit() { + parent := n.parent + if parent == nil { + return + } + if parent.Kind != NodeExpandSubject && parent.Kind != NodeTraverse { + return + } + if !n.Result.prunable() { + return + } + parent.mu.Lock() + defer parent.mu.Unlock() + parent.prunableCount++ + if parent.prunableCount <= MaxExpandChildren { + return + } + for i, c := range parent.Children { + if c == n { + parent.Children = append(parent.Children[:i], parent.Children[i+1:]...) + break + } + } + parent.TruncatedChildren++ +} + +func (n *Node) recordTuplesLoaded(count int) { + n.mu.Lock() + n.TuplesLoaded += count + n.mu.Unlock() +} + +func (n *Node) recordFoundSubject(tuple *relationtuple.RelationTuple) { + child := &Node{Kind: NodeFoundSubject, Tuple: tuple, Result: ResultMember} + n.mu.Lock() + n.Children = append(n.Children, child) + n.mu.Unlock() +} + +// String is a human-friendly representation of the debug tree, useful during testing. +func (n *Node) String() string { + if n == nil { + return "" + } + + var b strings.Builder + + var walk func(cur *Node, depth int) + walk = func(cur *Node, depth int) { + for range depth { + b.WriteString(" ") + } + + input := "" + if cur.Tuple != nil { + input = cur.Tuple.String() + if len(cur.Relations) > 0 { + input = strings.Replace(input, "#"+cur.Tuple.Relation+"@", "#{"+strings.Join(cur.Relations, ",")+"}"+"@", 1) + } + } + b.WriteString(string(cur.Kind)) + b.WriteByte(' ') + b.WriteString(strconv.Quote(input)) + b.WriteString(" -> ") + b.WriteString(string(cur.Result)) + + if cur.TuplesLoaded > 0 { + b.WriteString(" tuples_loaded=") + b.WriteString(strconv.Itoa(cur.TuplesLoaded)) + } + b.WriteByte('\n') + + for _, child := range cur.Children { + walk(child, depth+1) + } + } + + walk(n, 0) + return strings.TrimSuffix(b.String(), "\n") +} diff --git a/internal/check/trace/session.go b/internal/check/trace/session.go new file mode 100644 index 0000000..7137e08 --- /dev/null +++ b/internal/check/trace/session.go @@ -0,0 +1,147 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package trace + +import ( + "context" + "sync" + "time" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/step" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/relationtuple" +) + +// traceNodeKey is the context key used to store and retrieve the current +// trace node as execution descends through the step tree. +type traceNodeKey struct{} + +func nodeFromCtx(ctx context.Context) *Node { + n, _ := ctx.Value(traceNodeKey{}).(*Node) + return n +} + +func withTraceNode(ctx context.Context, n *Node) context.Context { + return context.WithValue(ctx, traceNodeKey{}, n) +} + +// Session collects the debug tree for a single runtime execution. +type Session struct { + mu sync.Mutex + root *Node + wg sync.WaitGroup +} + +func NewSession() *Session { + return &Session{} +} + +func (t *Session) Root() *Node { + t.mu.Lock() + defer t.mu.Unlock() + return t.root +} + +func (t *Session) Wait() { + t.wg.Wait() +} + +// nodeKindForStep maps a Step's execution-model kind to the trace-model NodeKind. +func nodeKindForStep(s check.Step) NodeKind { + if s, ok := s.(step.RewriteStep); ok { + if s.Rewrite.Operation == ast.OperatorOr { + return NodeUnion + } + return NodeIntersection + } + switch s.Kind() { + case check.StepIsAllowed: + return NodeIsAllowed + case check.StepDirect: + return NodeDirect + case check.StepDirectMulti: + return NodeMultiDirect + case check.StepExpand: + return NodeExpandSubject + case check.StepComputed: + return NodeComputed + case check.StepTraverse: + return NodeTraverse + case check.StepInvert: + return NodeInvert + default: + return NodeKind(s.Kind()) + } +} + +// Middleware returns a Middleware that records a Node for every step execution, +// building the debug trace tree as execution unfolds. +func (t *Session) Middleware() check.Middleware { + return func(ctx context.Context, s check.Step, req check.CheckRequest, next func(context.Context, check.Step, check.CheckRequest) check.Result) check.Result { + t.wg.Add(1) + defer t.wg.Done() + + tuple := req.Tuple + var relations []string + + switch s := s.(type) { + case step.ComputedUsersetStep: + clone := *req.Tuple + clone.Relation = s.Relation + tuple = &clone + case step.TraverseStep: + clone := *req.Tuple + clone.Relation = s.TTU.Relation + tuple = &clone + case step.DirectMultiStep: + relations = s.Relations + } + + node, ctx := t.newTraceNode(ctx, nodeKindForStep(s), tuple, relations...) + switch s.Kind() { + case check.StepExpand: + ctx = check.WithTraceHooks(ctx, check.TraceHooks{ + TuplesLoaded: node.recordTuplesLoaded, + SubjectFound: node.recordFoundSubject, + }) + case check.StepTraverse: + ctx = check.WithTraceHooks(ctx, check.TraceHooks{TuplesLoaded: node.recordTuplesLoaded}) + } + + start := time.Now() + result := next(ctx, s, req) + node.Duration = time.Since(start) + node.setResult(result) + node.dropIfOverLimit() + + return result + } +} + +// newTraceNode creates a Node, links it as a child of the current parent node +// in ctx (or sets it as the root if there is none), and returns both the node +// and a derived context with the new node set as the current parent. +func (t *Session) newTraceNode(ctx context.Context, kind NodeKind, tuple *relationtuple.RelationTuple, relations ...string) (*Node, context.Context) { + node := &Node{ + Kind: kind, + Tuple: tuple, + Relations: relations, + + // Initial Result is Skipped, as it's possible that the step won't run at all + // due to other sibling branch finishing with success. + Result: ResultSkipped, + } + if parent := nodeFromCtx(ctx); parent != nil { + node.parent = parent + parent.mu.Lock() + parent.Children = append(parent.Children, node) + parent.mu.Unlock() + } else { + t.mu.Lock() + t.root = node + t.mu.Unlock() + } + return node, withTraceNode(ctx, node) +} diff --git a/internal/check/trace/testhelpers.go b/internal/check/trace/testhelpers.go new file mode 100644 index 0000000..76c92ab --- /dev/null +++ b/internal/check/trace/testhelpers.go @@ -0,0 +1,115 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package trace + +import ( + "slices" + "strings" + "testing" + + "github.com/ory/keto/internal/testhelpers" +) + +// SortNode sorts children of NodeExpandSubject and NodeTraverse nodes by +// Tuple.String() recursively. Both node kinds have non-deterministic child +// order because the underlying queries order by shard_id (a random UUIDv4 +// assigned at write time). All other sibling orderings are engine-controlled +// and must be asserted as-is. +func SortNode(n *Node) *Node { + if n == nil { + return nil + } + if n.Kind == NodeExpandSubject || n.Kind == NodeTraverse { + slices.SortFunc(n.Children, func(a, b *Node) int { + as, bs := "", "" + if a.Tuple != nil { + as = a.Tuple.String() + } + if b.Tuple != nil { + bs = b.Tuple.String() + } + if c := strings.Compare(as, bs); c != 0 { + return c + } + return strings.Compare(string(a.Kind), string(b.Kind)) + }) + } + for _, child := range n.Children { + SortNode(child) + } + return n +} + +// StripTiming zeroes out Duration on every node in the tree so that test +// assertions are not sensitive to wall-clock time. +func StripTiming(n *Node) *Node { + if n == nil { + return nil + } + n.Duration = 0 + for _, child := range n.Children { + StripTiming(child) + } + return n +} + +// CheckNode builds a NodeIsAllowed node. +func CheckNode(t testing.TB, result NodeResult, tuple string, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeIsAllowed, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, Children: children} +} + +// DirectNode builds a NodeDirect leaf (no children). +func DirectNode(t testing.TB, result NodeResult, tuple string) *Node { + t.Helper() + return &Node{Kind: NodeDirect, Tuple: testhelpers.TupleFromString(t, tuple), Result: result} +} + +// ExpandNode builds a NodeExpandSubject node. +func ExpandNode(t testing.TB, result NodeResult, tuple string, tuplesLoaded int, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeExpandSubject, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, TuplesLoaded: tuplesLoaded, Children: children} +} + +// FoundNode builds a NodeFoundSubject leaf (no children). +func FoundNode(t testing.TB, tuple string) *Node { + t.Helper() + return &Node{Kind: NodeFoundSubject, Tuple: testhelpers.TupleFromString(t, tuple), Result: ResultMember} +} + +// UnionNode builds a NodeUnion node. +func UnionNode(t testing.TB, result NodeResult, tuple string, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeUnion, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, Children: children} +} + +// IntersectionNode builds a NodeIntersection node. +func IntersectionNode(t testing.TB, result NodeResult, tuple string, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeIntersection, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, Children: children} +} + +// InvertNode builds a NodeInvert node. +func InvertNode(t testing.TB, result NodeResult, tuple string, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeInvert, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, Children: children} +} + +// ComputedNode builds a NodeComputed node. +func ComputedNode(t testing.TB, result NodeResult, tuple string, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeComputed, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, Children: children} +} + +// TraverseNode builds a NodeTraverse node. +func TraverseNode(t testing.TB, result NodeResult, tuple string, tuplesLoaded int, children ...*Node) *Node { + t.Helper() + return &Node{Kind: NodeTraverse, Tuple: testhelpers.TupleFromString(t, tuple), Result: result, TuplesLoaded: tuplesLoaded, Children: children} +} + +// MultiDirectNode builds a NodeMultiDirect leaf (no children). +func MultiDirectNode(t testing.TB, result NodeResult, tuple string, relations []string) *Node { + t.Helper() + return &Node{Kind: NodeMultiDirect, Tuple: testhelpers.TupleFromString(t, tuple), Relations: relations, Result: result} +} diff --git a/internal/check/trace_hook.go b/internal/check/trace_hook.go new file mode 100644 index 0000000..ba09fc7 --- /dev/null +++ b/internal/check/trace_hook.go @@ -0,0 +1,44 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package check + +import ( + "context" + + "github.com/ory/keto/internal/relationtuple" +) + +type traceHooksKey struct{} + +// TraceHooks carries optional instrumentation callbacks injected into context +// by tracing middleware. Step implementations can emit events through helpers +// in this file without importing the trace package. +type TraceHooks struct { + TuplesLoaded func(n int) + SubjectFound func(tuple *relationtuple.RelationTuple) +} + +// WithTraceHooks returns a context that carries the given trace hooks. +func WithTraceHooks(ctx context.Context, hooks TraceHooks) context.Context { + return context.WithValue(ctx, traceHooksKey{}, hooks) +} + +func traceHooksFromContext(ctx context.Context) (TraceHooks, bool) { + hooks, ok := ctx.Value(traceHooksKey{}).(TraceHooks) + return hooks, ok +} + +// ReportTuplesLoaded calls the tuples-loaded hook stored in ctx, if any. +func ReportTuplesLoaded(ctx context.Context, n int) { + if hooks, ok := traceHooksFromContext(ctx); ok && hooks.TuplesLoaded != nil { + hooks.TuplesLoaded(n) + } +} + +// ReportSubjectFound calls the subject-found hook stored in ctx, if any. +func ReportSubjectFound(ctx context.Context, tuple *relationtuple.RelationTuple) { + if hooks, ok := traceHooksFromContext(ctx); ok && hooks.SubjectFound != nil { + hooks.SubjectFound(tuple) + } +} diff --git a/internal/driver/config/buildinfo.go b/internal/driver/config/buildinfo.go new file mode 100644 index 0000000..184d963 --- /dev/null +++ b/internal/driver/config/buildinfo.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +var ( + Version = "master" + Date = "undefined" + Commit = "undefined" +) diff --git a/internal/driver/config/namespace_memory.go b/internal/driver/config/namespace_memory.go new file mode 100644 index 0000000..215a011 --- /dev/null +++ b/internal/driver/config/namespace_memory.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "reflect" + "sync" + + "github.com/ory/herodot" + "github.com/pkg/errors" + + "github.com/ory/keto/internal/namespace" +) + +type ( + memoryNamespaceManager struct { + byName map[string]*namespace.Namespace + sync.RWMutex + } +) + +var _ namespace.Manager = &memoryNamespaceManager{} + +func NewMemoryNamespaceManager(nn ...*namespace.Namespace) *memoryNamespaceManager { + s := &memoryNamespaceManager{} + s.set(nn) + return s +} + +func (s *memoryNamespaceManager) GetNamespaceByName(_ context.Context, name string) (*namespace.Namespace, error) { + s.RLock() + defer s.RUnlock() + + if n, ok := s.byName[name]; ok { + return n, nil + } + + return nil, errors.WithStack(herodot.ErrNotFound().WithReasonf("Unknown namespace with name %q.", name)) +} + +func (s *memoryNamespaceManager) GetNamespaceByConfigID(_ context.Context, id int32) (*namespace.Namespace, error) { + s.RLock() + defer s.RUnlock() + + for _, n := range s.byName { + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + if n.ID == id { + return n, nil + } + } + + return nil, errors.WithStack(herodot.ErrNotFound().WithReasonf("Unknown namespace with id %d.", id)) +} + +func (s *memoryNamespaceManager) Namespaces(_ context.Context) ([]*namespace.Namespace, error) { + s.RLock() + defer s.RUnlock() + + nn := make([]*namespace.Namespace, 0, len(s.byName)) + for _, n := range s.byName { + nn = append(nn, n) + } + + return nn, nil +} + +func (s *memoryNamespaceManager) ShouldReload(newValue interface{}) bool { + s.RLock() + defer s.RUnlock() + + nn, _ := s.Namespaces(context.Background()) + + return !reflect.DeepEqual(newValue, nn) +} + +func (s *memoryNamespaceManager) set(nn []*namespace.Namespace) { + s.Lock() + defer s.Unlock() + + s.byName = make(map[string]*namespace.Namespace, len(nn)) + for _, n := range nn { + s.byName[n.Name] = n + } +} diff --git a/internal/driver/config/namespace_memory_test.go b/internal/driver/config/namespace_memory_test.go new file mode 100644 index 0000000..cc6b195 --- /dev/null +++ b/internal/driver/config/namespace_memory_test.go @@ -0,0 +1,25 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ory/keto/internal/namespace" +) + +func TestNamespaceMemoryManager(t *testing.T) { + t.Run("method=should reload", func(t *testing.T) { + n := &namespace.Namespace{Name: "2"} + nm := NewMemoryNamespaceManager(n) + + assert.False(t, nm.ShouldReload([]*namespace.Namespace{n})) + assert.False(t, nm.ShouldReload([]*namespace.Namespace{{Name: "2"}})) + + assert.True(t, nm.ShouldReload([]*namespace.Namespace{{Name: "3"}})) + assert.True(t, nm.ShouldReload("foo")) + }) +} diff --git a/internal/driver/config/namespace_watcher.go b/internal/driver/config/namespace_watcher.go new file mode 100644 index 0000000..77d531e --- /dev/null +++ b/internal/driver/config/namespace_watcher.go @@ -0,0 +1,295 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "sync" + + "github.com/ghodss/yaml" + "github.com/ory/herodot" + "github.com/ory/x/logrusx" + "github.com/ory/x/stringsx" + "github.com/ory/x/urlx" + "github.com/ory/x/watcherx" + "github.com/pelletier/go-toml" + "github.com/pkg/errors" + + "github.com/ory/keto/internal/namespace" +) + +type ( + Parser func([]byte, interface{}) error + + NamespaceFile struct { + Name string + Contents []byte + Parser Parser + + namespace *namespace.Namespace // last successfully parsed namespace. May be nil if there was a parse error + } + + NamespaceWatcher struct { + sync.RWMutex + namespaces map[string]*NamespaceFile + logger *logrusx.Logger + target string + } + + eventHandler interface { + handleRemove(*watcherx.RemoveEvent) + handleChange(*watcherx.ChangeEvent) + handleError(*watcherx.ErrorEvent) + } +) + +var _ namespace.Manager = (*NamespaceWatcher)(nil) + +func NewNamespaceWatcher(ctx context.Context, l *logrusx.Logger, target string) (*NamespaceWatcher, error) { + nw := NamespaceWatcher{ + logger: l, + target: target, + namespaces: make(map[string]*NamespaceFile), + } + + return &nw, watchTarget(ctx, target, &nw, l) +} + +func (nw *NamespaceWatcher) handleRemove(e *watcherx.RemoveEvent) { + nw.Lock() + defer nw.Unlock() + + delete(nw.namespaces, e.Source()) +} + +func (nw *NamespaceWatcher) handleChange(e *watcherx.ChangeEvent) { + // the lock is acquired before parsing to ensure that the getters are + // waiting for the updated values + nw.Lock() + defer nw.Unlock() + + n := nw.readNamespaceFile(e.Reader(), e.Source()) + if n == nil { + return + } else if n.namespace == nil { + // parse failed, rolling back to previous working version + if existing, ok := nw.namespaces[e.Source()]; ok { + existing.Contents = n.Contents + } else { + nw.namespaces[e.Source()] = n + } + } else { + nw.namespaces[e.Source()] = n + } +} + +func (nw *NamespaceWatcher) handleError(e *watcherx.ErrorEvent) { + nw.logger. + WithError(e). + Errorf("Received error while watching namespace files at target %s.", nw.target) +} + +func watchTarget(ctx context.Context, target string, handler eventHandler, log *logrusx.Logger) error { + var ( + eventCh = make(watcherx.EventChannel) + watcher watcherx.Watcher + ) + + targetUrl, err := urlx.Parse(target) + if err != nil { + return errors.WithStack(err) + } + info, err := os.Stat(targetUrl.Path) //nolint:gosec // Path is from a validated configuration URL, not user input. + if err != nil { + return errors.WithStack(err) + } + if info.IsDir() { + watcher, err = watcherx.WatchDirectory(ctx, urlx.GetURLFilePath(targetUrl), eventCh) + } else { + watcher, err = watcherx.Watch(ctx, targetUrl, eventCh) + } + // this handles the watcher init error + if err != nil { + return err + } + + // trigger initial load + done, err := watcher.DispatchNow() + if err != nil { + return errors.WithStack(err) + } + + initialEventsProcessed := make(chan struct{}) + go startEventHandler(ctx, eventCh, handler, done, initialEventsProcessed, log) + + // wait for initial load to be done + <-initialEventsProcessed + + return nil +} + +func startEventHandler(ctx context.Context, + eventCh watcherx.EventChannel, + handler eventHandler, + done <-chan int, + initialEventsProcessed chan<- struct{}, + log *logrusx.Logger) { + + initialDone := false + for { + select { + // because we use an unbuffered chan we can be sure that at least all + // initial events are handled + case <-done: + initialDone = true + close(initialEventsProcessed) + // Disable this case after it fires once. watcherx closes done when + // the watcher shuts down (e.g. directoryWatcher on ctx cancel); a + // closed channel is always ready, so leaving done set would re-enter + // this branch and close(initialEventsProcessed) a second time (panic). + done = nil + + case <-ctx.Done(): + return + + case e, open := <-eventCh: + if !open { + return + } + + if initialDone { + log. + WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("A change to a namespace file was detected.") + } + + switch e := e.(type) { + case *watcherx.RemoveEvent: + handler.handleRemove(e) + case *watcherx.ChangeEvent: + handler.handleChange(e) + case *watcherx.ErrorEvent: + handler.handleError(e) + default: + log.Warnf("Ignored unknown event %T", e) + } + } + } +} + +func (nw *NamespaceWatcher) readNamespaceFile(r io.Reader, source string) *NamespaceFile { + parse, err := GetParser(source) + if err != nil { + nw.logger. + WithError(err). + WithField("file_name", source). + Warn("could not infer format from file extension") + return nil + } + + raw, err := io.ReadAll(r) + if err != nil { + nw.logger. + WithError(errors.WithStack(err)). + WithField("file_name", source). + Error("could not read namespace file") + return nil + } + + n := namespace.Namespace{} + if err := parse(raw, &n); err != nil { + nw.logger. + WithError(errors.WithStack(err)). + WithField("file_name", source). + Error("could not parse namespace file") + return &NamespaceFile{Name: source, Contents: raw, Parser: parse} + } + + return &NamespaceFile{Name: source, Contents: raw, Parser: parse, namespace: &n} +} + +func (nw *NamespaceWatcher) GetNamespaceByName(_ context.Context, name string) (*namespace.Namespace, error) { + nw.RLock() + defer nw.RUnlock() + + for _, nsf := range nw.namespaces { + if nsf.namespace != nil && nsf.namespace.Name == name { + return nsf.namespace, nil + } + } + + return nil, errors.WithStack(herodot.ErrNotFound().WithErrorf( + "Unknown namespace with name %s", name)) +} + +func (nw *NamespaceWatcher) GetNamespaceByConfigID(_ context.Context, id int32) (*namespace.Namespace, error) { + nw.RLock() + defer nw.RUnlock() + + for _, nspace := range nw.namespaces { + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + if nspace.namespace.ID == id { + return nspace.namespace, nil + } + } + + return nil, errors.WithStack(herodot.ErrNotFound().WithErrorf( + "Unknown namespace with ID %d", id)) +} + +func (nw *NamespaceWatcher) Namespaces(_ context.Context) ([]*namespace.Namespace, error) { + nw.RLock() + defer nw.RUnlock() + + nspaces := make([]*namespace.Namespace, 0, len(nw.namespaces)) + for _, nsf := range nw.namespaces { + if nsf.namespace != nil { + nspaces = append(nspaces, nsf.namespace) + } + } + return nspaces, nil +} + +func (nw *NamespaceWatcher) NamespaceFiles() []*NamespaceFile { + nw.RLock() + defer nw.RUnlock() + + nsfs := make([]*NamespaceFile, 0, len(nw.namespaces)) + for _, nsf := range nw.namespaces { + nsfs = append(nsfs, nsf) + } + return nsfs +} + +func (nw *NamespaceWatcher) ShouldReload(newValue interface{}) bool { + v, ok := newValue.(string) + if !ok { + // the manager type changed + return true + } + // reload if target changed + return v != nw.target +} + +func GetParser(fn string) (Parser, error) { + switch ext := stringsx.SwitchExact(filepath.Ext(fn)); { + case ext.AddCase(".yaml"), ext.AddCase(".yml"): + return func(b []byte, i interface{}) error { + return yaml.Unmarshal(b, i) + }, nil + case ext.AddCase(".json"): + return json.Unmarshal, nil + case ext.AddCase(".toml"): + return toml.Unmarshal, nil + default: + return nil, ext.ToUnknownCaseErr() + } +} diff --git a/internal/driver/config/namespace_watcher_test.go b/internal/driver/config/namespace_watcher_test.go new file mode 100644 index 0000000..d2262f6 --- /dev/null +++ b/internal/driver/config/namespace_watcher_test.go @@ -0,0 +1,250 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/ghodss/yaml" + "github.com/ory/x/logrusx" + "github.com/ory/x/watcherx" + "github.com/pelletier/go-toml" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/namespace" +) + +func TestNamespaceProvider(t *testing.T) { + setup := func(t *testing.T, target string) (*NamespaceWatcher, *test.Hook) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + hook := &test.Hook{} + l := logrusx.New("", "", logrusx.WithHook(hook)) + + nw, err := NewNamespaceWatcher(ctx, l, target) + require.NoError(t, err) + + return nw, hook + } + + writeNamespace := func(t *testing.T, n *namespace.Namespace, fn string) { + var marshal func(interface{}) ([]byte, error) + switch ext := filepath.Ext(fn); ext { + case ".yaml", ".yml": + marshal = yaml.Marshal + case ".toml": + marshal = toml.Marshal + case ".json": + marshal = json.Marshal + default: + t.Logf("got unexpected file extension %s", ext) + t.FailNow() + } + nEnc, err := marshal(n) + require.NoError(t, err) + require.NoError(t, os.WriteFile(fn, nEnc, 0600)) + } + + writeDir := func(t *testing.T, dir string, files map[string]interface{}) { + for fn, fc := range files { + fp := filepath.Join(dir, fn) + require.NoError(t, os.MkdirAll(filepath.Dir(fp), 0700)) + + if n, ok := fc.(*namespace.Namespace); ok { + writeNamespace(t, n, fp) + continue + } + + s, ok := fc.(string) + if !ok { + t.Logf("expected file content to be string or *namespace.Namespace but got %T", fc) + t.FailNow() + } + + require.NoError(t, os.WriteFile(fp, []byte(s), 0600)) + } + } + + writeJsonNamespace := func(t *testing.T) (string, *namespace.Namespace) { + dir := t.TempDir() + n := &namespace.Namespace{ + Name: "test namespace 1", + } + fn := filepath.Join(dir, "n.json") + + writeNamespace(t, n, fn) + + return fn, n + } + + t.Run("case=loads JSON namespace file", func(t *testing.T) { + fn, n := writeJsonNamespace(t) + + ws, _ := setup(t, "file://"+fn) + + nspaces, err := ws.Namespaces(context.Background()) + require.NoError(t, err) + + assert.Equal(t, []*namespace.Namespace{n}, nspaces) + }) + + t.Run("case=reads namespace files from directory", func(t *testing.T) { + dir := t.TempDir() + files := map[string]interface{}{ + "b.yml": &namespace.Namespace{ + Name: "b", + }, + "a.toml": &namespace.Namespace{ + Name: "a", + }, + "c.json": &namespace.Namespace{ + Name: "c", + }, + } + + writeDir(t, dir, files) + + nw, _ := setup(t, "file://"+dir) + + nspaces, err := nw.Namespaces(context.Background()) + require.NoError(t, err) + + for _, n := range files { + assert.Contains(t, nspaces, n.(*namespace.Namespace)) + } + + nsfs := nw.NamespaceFiles() + assert.Equal(t, len(nspaces), len(nsfs)) + for _, n := range nsfs { + assert.NotNil(t, n.Contents) + assert.NotNil(t, n.Parser) + } + }) + + t.Run("case=ignores but warns about unsupported file extensions", func(t *testing.T) { + dir := t.TempDir() + + n := &namespace.Namespace{ + Name: "some name", + } + nJson, err := json.Marshal(n) + require.NoError(t, err) + + writeDir(t, dir, map[string]interface{}{ + "unsupported.file": "foo bar\n", + "supported.json": string(nJson), + }) + + nw, hook := setup(t, "file://"+dir) + + require.Len(t, hook.Entries, 1) + + assert.Equal(t, logrus.WarnLevel, hook.Entries[0].Level, "%+v", hook.Entries[0]) + assert.True(t, strings.HasSuffix(hook.Entries[0].Data["file_name"].(string), "unsupported.file")) + + namespaces, err := nw.Namespaces(context.Background()) + require.NoError(t, err) + + require.Len(t, namespaces, 1) + assert.Equal(t, n, namespaces[0]) + }) + + t.Run("case=still returns successful namespace if one errors", func(t *testing.T) { + dir := t.TempDir() + + n := &namespace.Namespace{ + Name: "some name", + } + nJson, err := json.Marshal(n) + require.NoError(t, err) + + writeDir(t, dir, map[string]interface{}{ + "malformed.yml": "foo bar\n", + "correct.json": string(nJson), + }) + + nw, hook := setup(t, "file://"+dir) + + require.Len(t, hook.Entries, 1) + + assert.Equal(t, logrus.ErrorLevel, hook.Entries[0].Level, "%+v", hook.Entries[0]) + assert.True(t, strings.HasSuffix(hook.Entries[0].Data["file_name"].(string), "malformed.yml")) + + namespaces, err := nw.Namespaces(context.Background()) + require.NoError(t, err) + + require.Len(t, namespaces, 1) + assert.Equal(t, n, namespaces[0]) + + // files are included even if ns is unparsable + nsfs := nw.NamespaceFiles() + assert.Equal(t, 2, len(nsfs)) + }) + + t.Run("method=should reload", func(t *testing.T) { + nw := &NamespaceWatcher{ + target: "foo", + } + assert.False(t, nw.ShouldReload("foo")) + assert.True(t, nw.ShouldReload("bar")) + assert.True(t, nw.ShouldReload([]*namespace.Namespace{})) + }) +} + +type noopEventHandler struct{} + +func (noopEventHandler) handleRemove(*watcherx.RemoveEvent) {} +func (noopEventHandler) handleChange(*watcherx.ChangeEvent) {} +func (noopEventHandler) handleError(*watcherx.ErrorEvent) {} + +func TestStartEventHandlerDoesNotCloseInitialDoneTwice(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + t.Cleanup(cancel) + + eventCh := make(watcherx.EventChannel) + done := make(chan int, 1) + initialEventsProcessed := make(chan struct{}) + handlerReturned := make(chan any, 1) + + go func() { + defer func() { + handlerReturned <- recover() + }() + + startEventHandler(ctx, eventCh, noopEventHandler{}, done, initialEventsProcessed, logrusx.New("", "")) + }() + + done <- 0 + select { + case <-initialEventsProcessed: + case <-time.After(time.Second): + t.Fatal("timed out waiting for initial events to be processed") + } + + close(done) + select { + case recovered := <-handlerReturned: + require.Nil(t, recovered) + t.Fatal("event handler returned before context cancellation") + case <-time.After(100 * time.Millisecond): + } + + cancel() + + select { + case recovered := <-handlerReturned: + require.Nil(t, recovered) + case <-time.After(time.Second): + t.Fatal("timed out waiting for event handler to stop") + } +} diff --git a/internal/driver/config/opl_cache_test.go b/internal/driver/config/opl_cache_test.go new file mode 100644 index 0000000..15a24e4 --- /dev/null +++ b/internal/driver/config/opl_cache_test.go @@ -0,0 +1,193 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + "testing/synctest" + + "github.com/hashicorp/go-retryablehttp" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + + "github.com/ory/herodot" + "github.com/ory/x/configx" + "github.com/ory/x/fetcher" + "github.com/ory/x/logrusx" +) + +func TestNewOPLConfigWatcher(t *testing.T) { + var hits atomic.Int64 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + if _, err := io.WriteString(w, testOPL); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + })) + t.Cleanup(ts.Close) + ctx := context.Background() + cfg, err := NewDefault(ctx, nil, logrusx.New("", ""), configx.SkipValidation()) + require.NoError(t, err) + cw, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err) + require.EqualValues(t, 1, hits.Load(), "HTTP request made") + _, err = cw.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + _, err = cw.GetNamespaceByName(ctx, "Document") + require.NoError(t, err) + + cache.Wait() + + cw, err = newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err) + require.EqualValues(t, 1, hits.Load(), "content was cached") + _, err = cw.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + _, err = cw.GetNamespaceByName(ctx, "Document") + require.NoError(t, err) +} + +func TestOPLConfigWatcher(t *testing.T) { + t.Parallel() + + t.Run("caches parsed namespaces", func(t *testing.T) { + t.Parallel() + ts, hits := newCountingServer(t, http.StatusOK, testOPL) + cfg := newTestConfig(t) + ctx := t.Context() + + first, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err) + cache.Wait() + + second, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err) + require.EqualValues(t, 1, hits.Load(), "content was fetched only once") + + firstUser, err := first.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + secondUser, err := second.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + require.Same(t, firstUser, secondUser, "the parsed namespaces are shared, not re-parsed per construction") + }) + + t.Run("deduplicates concurrent fetches", func(t *testing.T) { + const parallel = 5 + const target = "https://opl.invalid/namespaces.ts" + // The cache is process-global and outlives the test binary's runs. + cache.Del(target) + // The config warms package-level caches whose goroutines live outside + // the bubble, so build it here. + cfg := newTestConfig(t) + + synctest.Test(t, func(t *testing.T) { + var hits atomic.Int64 + release := make(chan struct{}) + hc := retryablehttp.NewClient() + hc.Logger = nil + hc.HTTPClient = &http.Client{Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) { + hits.Add(1) + <-release + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(testOPL)), + Header: make(http.Header), + Request: r, + }, nil + })} + newFetcher := func() *fetcher.Fetcher { return fetcher.NewFetcher(fetcher.WithClient(hc)) } + ctx := t.Context() + + watchers := make([]*oplConfigWatcher, parallel) + var eg errgroup.Group + for i := range parallel { + eg.Go(func() error { + cw, err := newOPLConfigWatcher(ctx, cfg.l, newFetcher, target) + watchers[i] = cw + return err + }) + } + synctest.Wait() // One caller is inside the fetch, the rest joined its flight. + close(release) + + require.NoError(t, eg.Wait()) + require.EqualValues(t, 1, hits.Load(), "concurrent constructions share a single fetch") + + for _, cw := range watchers { + _, err := cw.GetNamespaceByName(ctx, "User") + require.NoError(t, err) + } + }) + }) + + t.Run("retries failed fetches", func(t *testing.T) { + t.Parallel() + ts, hits := newCountingServer(t, http.StatusNotFound, "") + cfg := newTestConfig(t) + ctx := t.Context() + + _, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.ErrorContains(t, err, "404") + cache.Wait() + + _, err = newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.ErrorContains(t, err, "404") + require.EqualValues(t, 2, hits.Load(), "failed fetches are not cached") + }) + + t.Run("caches parse failures", func(t *testing.T) { + t.Parallel() + ts, hits := newCountingServer(t, http.StatusOK, "/* unclosed comment") + cfg := newTestConfig(t) + ctx := t.Context() + + first, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err, "a parse failure does not fail construction") + _, err = first.GetNamespaceByName(ctx, "User") + require.ErrorIs(t, err, herodot.ErrNotFound()) + cache.Wait() + + second, err := newOPLConfigWatcher(ctx, cfg.l, cfg.Fetcher, ts.URL) + require.NoError(t, err) + require.EqualValues(t, 1, hits.Load(), "a parse failure is cached and does not trigger a fetch per construction") + _, err = second.GetNamespaceByName(ctx, "User") + require.ErrorIs(t, err, herodot.ErrNotFound()) + }) +} + +func newTestConfig(t *testing.T) *Config { + t.Helper() + cfg, err := NewDefault(t.Context(), nil, logrusx.New("", ""), configx.SkipValidation()) + require.NoError(t, err) + return cfg +} + +func newCountingServer(t *testing.T, status int, body string) (*httptest.Server, *atomic.Int64) { + t.Helper() + hits := new(atomic.Int64) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) + w.WriteHeader(status) + _, _ = io.WriteString(w, body) + })) + t.Cleanup(ts.Close) + return ts, hits +} + +var testOPL = ` +import { Namespace } from "@ory/keto-namespace-types" + +class User implements Namespace {} +class Document implements Namespace {} +` + +type roundTripFunc func(*http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } diff --git a/internal/driver/config/opl_config_namespace_watcher.go b/internal/driver/config/opl_config_namespace_watcher.go new file mode 100644 index 0000000..40d4196 --- /dev/null +++ b/internal/driver/config/opl_config_namespace_watcher.go @@ -0,0 +1,181 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "bytes" + "context" + "fmt" + "io" + "sync" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/ory/x/fetcher" + "github.com/ory/x/logrusx" + "github.com/ory/x/urlx" + "github.com/ory/x/watcherx" + "github.com/pkg/errors" + "golang.org/x/sync/singleflight" + + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/schema" +) + +type ( + configFiles struct { + byPath map[string]io.Reader + sync.Mutex + } + + oplConfigWatcher struct { + logger *logrusx.Logger + target string + files configFiles + + memoryNamespaceManager + } +) + +var ( + _ namespace.Manager = (*oplConfigWatcher)(nil) + + // cache holds the parsed namespaces per remote OPL location. Content at a location is + // assumed to be immutable, therefore, a change to the OPL should create a new file location, + // therefore a new cache entry. + cache, _ = ristretto.NewCache(&ristretto.Config[string, []*namespace.Namespace]{ + MaxCost: 20_000_000, // 20 MB of estimated heap, each item ca. 10 KB => max 2000 items + NumCounters: 20_000, // max 2000 items => 20000 counters + BufferItems: 64, + Cost: namespace.EstimatedSize, + }) + + fetchGroup singleflight.Group +) + +func newOPLConfigWatcher(ctx context.Context, l *logrusx.Logger, newFetcher func() *fetcher.Fetcher, target string) (*oplConfigWatcher, error) { + nw := &oplConfigWatcher{ + logger: l, + target: target, + files: configFiles{byPath: make(map[string]io.Reader)}, + memoryNamespaceManager: *NewMemoryNamespaceManager(), + } + + targetUrl, err := urlx.Parse(target) + if err != nil { + return nil, errors.WithStack(err) + } + + switch targetUrl.Scheme { + case "file", "": + return nw, watchTarget(ctx, target, nw, l) + case "base64": + file, err := newFetcher().FetchContext(ctx, target) + if err != nil { + return nil, err + } + nw.files.Lock() + defer nw.files.Unlock() + nw.files.byPath[targetUrl.String()] = file + nw.parseFiles() + return nw, err + case "http", "https": + if namespaces, ok := cache.Get(target); ok { + nw.set(namespaces) + return nw, nil + } + v, err, _ := fetchGroup.Do(target, func() (any, error) { + // Detach from the leader's context so that its cancellation does not + // fail the concurrent callers sharing this fetch. + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second) + defer cancel() + + buf, err := newFetcher().FetchContext(ctx, target) + if err != nil { + return nil, err + } + b := buf.Bytes() + nw.files.Lock() + defer nw.files.Unlock() + nw.files.byPath[targetUrl.String()] = bytes.NewReader(b) + namespaces, ok := nw.parseFiles() + if !ok { + // Cache a parse failure as an empty result, so that a broken + // OPL file does not trigger a fetch per construction. + namespaces = []*namespace.Namespace{} + } + // Cost 0 lets the cache's Cost function compute the entry size. + cache.SetWithTTL(target, namespaces, 0, 30*time.Minute) + return namespaces, nil + }) + if err != nil { + return nil, err + } + if namespaces, ok := v.([]*namespace.Namespace); ok { + nw.set(namespaces) + } + return nw, nil + default: + return nil, fmt.Errorf("unexpected url scheme: %q", targetUrl.Scheme) + } +} + +func (nw *oplConfigWatcher) handleChange(e *watcherx.ChangeEvent) { + // the lock is acquired before parsing to ensure that the getters are + // waiting for the updated values + nw.files.Lock() + defer nw.files.Unlock() + nw.files.byPath[e.Source()] = e.Reader() + nw.parseFiles() +} + +func (nw *oplConfigWatcher) handleRemove(e *watcherx.RemoveEvent) { + nw.files.Lock() + defer nw.files.Unlock() + delete(nw.files.byPath, e.Source()) + nw.parseFiles() +} + +func (nw *oplConfigWatcher) handleError(e *watcherx.ErrorEvent) { + nw.logger. + WithError(e). + Errorf("Received error while watching OPL config files at target %s.", + nw.target) +} + +// parseFiles loops through all files, parsing each and getting the namespaces. +// It then sets and returns the namespaces only if there were no errors. +// +// The caller must hold the lock to nw.files. +func (nw *oplConfigWatcher) parseFiles() ([]*namespace.Namespace, bool) { + var ( + namespaces = make([]*namespace.Namespace, 0) + errs []error + ) + for _, reader := range nw.files.byPath { + content, err := io.ReadAll(reader) + if err != nil { + errs = append(errs, err) + continue + } + nn, ee := schema.Parse(string(content)) + for _, e := range ee { + errs = append(errs, e) + } + for _, n := range nn { + namespaces = append(namespaces, &n) + } + } + if len(errs) > 0 { + for _, err := range errs { + nw.logger. + WithError(err). + Errorf("Failed to parse OPL config files at target %s.", + nw.target) + } + return nil, false + } + nw.set(namespaces) + return namespaces, true +} diff --git a/internal/driver/config/provider.go b/internal/driver/config/provider.go new file mode 100644 index 0000000..89f3e54 --- /dev/null +++ b/internal/driver/config/provider.go @@ -0,0 +1,397 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "crypto/sha512" + _ "embed" + "encoding/json" + "fmt" + "sync" + + "github.com/ory/herodot" + _ "github.com/ory/jsonschema/v3/httploader" + "github.com/ory/x/configx" + "github.com/ory/x/fetcher" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/watcherx" + "github.com/pkg/errors" + "github.com/rs/cors" + "github.com/spf13/pflag" + + "github.com/ory/keto/embedx" + "github.com/ory/keto/internal/namespace" +) + +type EndpointType string + +const ( + EndpointRead EndpointType = "read" + EndpointWrite EndpointType = "write" + EndpointMetrics EndpointType = "metrics" + EndpointOPLSyntax EndpointType = "opl" + + KeyDSN = "dsn" + + KeyLimitMaxReadDepth = "limit.max_read_depth" + KeyLimitMaxReadWidth = "limit.max_read_width" + KeyLimitExpandMaxSize = "limit.max_expand_size" + + KeyBatchCheckMaxBatchSize = "limit.max_batch_check_size" + KeyBatchCheckParallelizationLimit = "limit.batch_check_max_parallelization" + + KeyReadAPIHost = "serve." + string(EndpointRead) + ".host" + KeyReadAPIPort = "serve." + string(EndpointRead) + ".port" + KeyReadAPIListenFile = "serve." + string(EndpointRead) + ".write_listen_file" + KeyWriteAPIHost = "serve." + string(EndpointWrite) + ".host" + KeyWriteAPIPort = "serve." + string(EndpointWrite) + ".port" + KeyWriteAPIListenFile = "serve." + string(EndpointWrite) + ".write_listen_file" + KeyOPLSyntaxAPIHost = "serve." + string(EndpointOPLSyntax) + ".host" + KeyOPLSyntaxAPIPort = "serve." + string(EndpointOPLSyntax) + ".port" + KeyOPLSyntaxListenFile = "serve." + string(EndpointOPLSyntax) + ".write_listen_file" + KeyMetricsHost = "serve." + string(EndpointMetrics) + ".host" + KeyMetricsPort = "serve." + string(EndpointMetrics) + ".port" + KeyMetricsListenFile = "serve." + string(EndpointMetrics) + ".write_listen_file" + + KeyNamespaces = "namespaces" + KeyNamespacesExperimentalStrictMode = KeyNamespaces + ".experimental_strict_mode" + + KeyFeatureFlagExpandRewrites = "feature_flags.expand_rewrites" + KeyFeatureFlagStrictMode = "feature_flags.strict_mode" + + DSNMemory = "sqlite://file::memory:?_fk=true&" + + KeySecretsPagination = "secrets.pagination" +) + +type ( + Config struct { + p *configx.Provider + l *logrusx.Logger + ctx context.Context + + nm namespace.Manager + cancelNamespaceManager context.CancelFunc + nmLock sync.Mutex + } + Provider interface { + Config(ctx context.Context) *Config + } +) + +func New(ctx context.Context, l *logrusx.Logger, p *configx.Provider) *Config { + return &Config{ + p: p, + l: l, + ctx: ctx, + } +} + +func NewDefault(ctx context.Context, flags *pflag.FlagSet, l *logrusx.Logger, opts ...configx.OptionModifier) (*Config, error) { + c := New(ctx, l, nil) + cp, err := NewProvider(ctx, flags, c, opts...) + if err != nil { + return nil, err + } + c.WithSource(cp) + + return c, nil +} + +func NewProvider(ctx context.Context, flags *pflag.FlagSet, config *Config, opts ...configx.OptionModifier) (*configx.Provider, error) { + p, err := configx.New( + ctx, + embedx.ConfigSchema, + append(opts, + configx.WithFlags(flags), + configx.WithStderrValidationReporter(), + configx.WithImmutables(KeyDSN, "serve"), + configx.OmitKeysFromTracing(KeyDSN), + configx.WithLogrusWatcher(config.l), + configx.WithContext(ctx), + configx.AttachWatcher(config.namespaceWatcher), + )..., + ) + if err != nil { + return nil, err + } + + return p, nil +} + +func (k *Config) Source() *configx.Provider { + return k.p +} + +func (k *Config) WithSource(p *configx.Provider) { + k.p = p + k.l.UseConfig(p) +} + +func (k *Config) namespaceWatcher(_ watcherx.Event, err error) { + if err != nil { + return + } + nm, err := k.NamespaceManager() + if err != nil { + k.l.WithError(err).Error("got internal error in config namespace watcher: could not get namespace manager") + return + } + + nnCfg, err := k.namespaceConfig() + if err != nil { + k.l.WithError(err).Error("could not get namespaces from config") + return + } + if nm.ShouldReload(nnCfg.value()) { + k.resetNamespaceManager() + } +} + +func (k *Config) resetNamespaceManager() { + k.nmLock.Lock() + defer k.nmLock.Unlock() + + if k.cancelNamespaceManager == nil { + return + } + + // cancel and delete old namespace manager + // the next read request will result in a new one being created + k.cancelNamespaceManager() + k.nm, k.cancelNamespaceManager = nil, nil +} + +func (k *Config) Set(key string, v any) error { + if err := k.p.Set(key, v); err != nil { + return err + } + + if key == KeyNamespaces { + k.resetNamespaceManager() + } + return nil +} + +func (k *Config) addressFor(endpoint EndpointType) (addr string, listenFile string) { + return fmt.Sprintf( + "%s:%d", + k.p.StringF("serve."+string(endpoint)+".host", ""), + k.p.IntF("serve."+string(endpoint)+".port", 0), + ), k.p.StringF("serve."+string(endpoint)+".write_listen_file", "") +} + +func (k *Config) ReadAPIListenOn() (addr string, listenFile string) { + return k.addressFor(EndpointRead) +} + +func (k *Config) WriteAPIListenOn() (addr string, listenFile string) { + return k.addressFor(EndpointWrite) +} + +func (k *Config) MetricsListenOn() (addr string, listenFile string) { + return k.addressFor(EndpointMetrics) +} + +func (k *Config) OPLSyntaxAPIListenOn() (addr string, listenFile string) { + return k.addressFor(EndpointOPLSyntax) +} + +func (k *Config) MaxReadDepth() int { + return k.p.Int(KeyLimitMaxReadDepth) +} + +func (k *Config) MaxReadWidth() int { + return k.p.Int(KeyLimitMaxReadWidth) +} + +func (k *Config) BatchCheckMaxBatchSize() int { + return k.p.Int(KeyBatchCheckMaxBatchSize) +} + +func (k *Config) BatchCheckParallelizationLimit() int { + return k.p.Int(KeyBatchCheckParallelizationLimit) +} + +func (k *Config) ExpandMaxTupleCount() int { + return k.p.Int(KeyLimitExpandMaxSize) +} + +func (k *Config) CORS(iface string) (cors.Options, bool) { + switch iface { + case "read", "write", "metrics": + default: + panic("expected interface 'read' or 'write', but got unknown interface " + iface) + } + + return k.p.CORS("serve."+iface, cors.Options{ + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, + AllowedHeaders: []string{"Authorization", "Content-Type"}, + ExposedHeaders: []string{"Content-Type"}, + AllowCredentials: true, + }) +} + +func (k *Config) DSN() string { + dsn := k.p.String(KeyDSN) + if dsn == "memory" { + return DSNMemory + } + return dsn +} + +func (k *Config) Fetcher() *fetcher.Fetcher { + // Tracing still works correctly even though we pass a no-op tracer + // here, because the otelhttp package will preferentially use the + // tracer from the incoming request context over this one. + opts := []httpx.ResilientOptions{} + if k.p.Bool("clients.http.disallow_private_ip_ranges") { + opts = append(opts, httpx.ResilientClientDisallowInternalIPs()) + } + return fetcher.NewFetcher( + fetcher.WithClient(httpx.NewResilientClient(opts...)), + ) +} + +func (k *Config) TracingServiceName() string { + return k.p.StringF("tracing.service_name", "Ory Keto") +} + +func (k *Config) TracingProvider() string { + return k.p.StringF("tracing.provider", "") +} + +func (k *Config) TracingConfig() *otelx.Config { + return k.p.TracingConfig("Ory Keto") +} + +func (k *Config) NamespaceManager() (namespace.Manager, error) { + k.nmLock.Lock() + defer k.nmLock.Unlock() + + if k.nm == nil { + var ctx context.Context + ctx, k.cancelNamespaceManager = context.WithCancel(k.ctx) + + nnCfg, err := k.namespaceConfig() + if err != nil { + return nil, err + } + + k.nm, err = nnCfg.newManager()(ctx, k) + if err != nil { + return nil, err + } + } + + return k.nm, nil +} + +func (k *Config) StrictMode() bool { + return k.p.Bool(KeyFeatureFlagStrictMode) || k.p.Bool(KeyNamespacesExperimentalStrictMode) +} + +func (k *Config) ExpandRewrites() bool { + return k.p.Bool(KeyFeatureFlagExpandRewrites) +} + +type ( + buildNamespaceFn func(context.Context, *Config) (namespace.Manager, error) + + namespaceConfig interface { + // newManager builds a new namespace manager. + newManager() buildNamespaceFn + // value returns the wrapped value (for comparing if we should reload) + value() any + } + + legacyURINamespaceConfig string + literalNamespaceConfig []*namespace.Namespace + oplNamespaceConfig map[string]any +) + +func (uri legacyURINamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, c *Config) (namespace.Manager, error) { + return NewNamespaceWatcher(ctx, c.l, string(uri)) + } +} + +func (uri legacyURINamespaceConfig) value() any { + return string(uri) +} + +func (namespaces literalNamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, _ *Config) (namespace.Manager, error) { + return NewMemoryNamespaceManager(namespaces...), nil + } +} + +func (namespaces literalNamespaceConfig) value() any { + return []*namespace.Namespace(namespaces) +} + +func (oplConfig oplNamespaceConfig) newManager() buildNamespaceFn { + return func(ctx context.Context, c *Config) (namespace.Manager, error) { + entry, ok := oplConfig["location"] + if !ok { + return nil, errors.New("location key not found") + } + target, ok := entry.(string) + if !ok { + return nil, fmt.Errorf("config value must be string, was %T", entry) + } + return newOPLConfigWatcher(ctx, c.l, c.Fetcher, target) + } +} + +func (oplConfig oplNamespaceConfig) value() any { + return map[string]any(oplConfig) +} + +// namespaceConfig returns a namespace config, which can be either a URI (in +// which case we want to watch that URI), or a literal list of namespaces (in +// which case we just load them into memory), or a list of URIs referencing OPL +// definitions (in which case we want to watch each URI and parse the content). +func (k *Config) namespaceConfig() (namespaceConfig, error) { + switch nTyped := k.p.GetF(KeyNamespaces, "file://./keto_namespaces").(type) { + case string: + return legacyURINamespaceConfig(nTyped), nil + + case []*namespace.Namespace: + return literalNamespaceConfig(nTyped), nil + + case []any: + nEnc, err := json.Marshal(nTyped) + if err != nil { + return nil, errors.WithStack(err) + } + + nn := make([]*namespace.Namespace, len(nTyped)) + + if err := json.Unmarshal(nEnc, &nn); err != nil { + return nil, errors.WithStack(err) + } + return literalNamespaceConfig(nn), nil + + case map[string]any: + return oplNamespaceConfig(nTyped), nil + + default: + return nil, errors.WithStack(herodot.ErrInternalServerError().WithReasonf("could not infer namespaces for type %T", nTyped)) + } +} + +func (k *Config) PaginationEncryptionKeys() [][32]byte { + secrets := k.p.Strings(KeySecretsPagination) + + encryptionKeys := make([][32]byte, len(secrets)) + for i, key := range secrets { + encryptionKeys[i] = sha512.Sum512_256([]byte(key)) + } + + return encryptionKeys +} diff --git a/internal/driver/config/provider_test.go b/internal/driver/config/provider_test.go new file mode 100644 index 0000000..8e8ebef --- /dev/null +++ b/internal/driver/config/provider_test.go @@ -0,0 +1,302 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package config + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "os" + "testing" + "time" + + "github.com/gobuffalo/httptest" + "github.com/ory/x/configx" + "github.com/ory/x/logrusx" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/embedx" + "github.com/ory/keto/internal/namespace" +) + +// createFile writes the content to a temporary file, returning the path. +// Good for testing config files. +func createFile(t *testing.T, content string) (path string) { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "config-*.yaml") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { _ = os.Remove(f.Name()) }) //nolint:gosec // Test file path from os.CreateTemp, not user input. + + n, err := f.WriteString(content) + if err != nil { + t.Fatal(err) + } + if n != len(content) { + t.Fatal("failed to write the complete content") + } + + return f.Name() +} + +// createFileF writes the content format string with the applied args to a +// temporary file, returning the path. Good for testing config files. +func createFileF(t *testing.T, contentF string, args ...any) (path string) { + return createFile(t, fmt.Sprintf(contentF, args...)) +} + +func setup(t *testing.T, configFile string) (*test.Hook, *Config) { + t.Helper() + hook := test.Hook{} + l := logrusx.New("test", "today", logrusx.WithHook(&hook)) + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + t.Cleanup(cancel) + + config, err := NewDefault( + ctx, + pflag.NewFlagSet("test", pflag.ContinueOnError), + l, + configx.WithConfigFiles(configFile), + ) + require.NoError(t, err) + + return &hook, config +} + +func assertNamespaces(t *testing.T, p *Config, nn ...*namespace.Namespace) { + t.Helper() + + nm, err := p.NamespaceManager() + require.NoError(t, err) + actualNamespaces, err := nm.Namespaces(context.Background()) + require.NoError(t, err) + assert.Equal(t, len(nn), len(actualNamespaces)) + assert.ElementsMatch(t, nn, actualNamespaces) +} + +// The new way to configure namespaces is through the Ory Permissions Language. +// We check here that we still support enumerating the namespaces directly in +// the config or through a file reference, in which case there should be no +// rewrites configured. +func TestLegacyNamespaceConfig(t *testing.T) { + t.Run("case=creates memory namespace manager when namespaces are set", func(t *testing.T) { + config := createFile(t, ` +dsn: memory +namespaces: + - name: n0 + - name: n1 + - name: n2`) + + run := func(namespaces []*namespace.Namespace) func(*testing.T) { + return func(t *testing.T) { + _, p := setup(t, config) + + assertNamespaces(t, p, namespaces...) + + nm, err := p.NamespaceManager() + require.NoError(t, err) + _, ok := nm.(*memoryNamespaceManager) + assert.True(t, ok) + assert.False(t, p.StrictMode()) + } + } + + nn := []*namespace.Namespace{ + {Name: "n0"}, + {Name: "n1"}, + {Name: "n2"}, + } + nnJson, err := json.Marshal(nn) + require.NoError(t, err) + nnValue := make([]interface{}, 0) + require.NoError(t, json.Unmarshal(nnJson, &nnValue)) + + t.Run( + "type=[]*namespace.Namespace", + run(nn), + ) + }) + + t.Run("case=reloads namespace manager when namespaces are updated using Set()", func(t *testing.T) { + _, p := setup(t, createFile(t, "dsn: memory")) + + n0 := &namespace.Namespace{ + Name: "n0", + } + n1 := &namespace.Namespace{ + Name: "n1", + } + + require.NoError(t, p.Set(KeyNamespaces, []*namespace.Namespace{n0})) + assertNamespaces(t, p, n0) + + require.NoError(t, p.Set(KeyNamespaces, []*namespace.Namespace{n1})) + assertNamespaces(t, p, n1) + }) + + t.Run("case=creates watcher manager when namespaces is string URL", func(t *testing.T) { + _, p := setup(t, createFileF(t, ` +dsn: memory +namespaces: file://%s`, + t.TempDir())) + + nm, err := p.NamespaceManager() + require.NoError(t, err) + _, ok := nm.(*NamespaceWatcher) + assert.True(t, ok) + assert.False(t, p.StrictMode()) + }) + + t.Run("case=uses passed configx provider", func(t *testing.T) { + ctx := context.Background() + cp, err := configx.New(ctx, embedx.ConfigSchema, configx.WithValue(KeyDSN, "foobar")) + require.NoError(t, err) + + p := New(ctx, logrusx.New("test", "today"), cp) + assert.Equal(t, "foobar", p.DSN()) + assert.Same(t, cp, p.p) + }) +} + +func TestInvalidNamespaceConfig(t *testing.T) { + config := createFile(t, ` +dsn: memory +namespaces: + - name: n0 + id: 2147483648 +`) + hook := test.Hook{} + l := logrusx.New("test", "today", logrusx.WithHook(&hook)) + _, err := NewDefault( + context.Background(), + pflag.NewFlagSet("test", pflag.ContinueOnError), + l, + configx.WithConfigFiles(config), + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "must be <= 2.147483647e+09 but found 2147483648") +} + +// Test that the namespaces can be configured through the Ory Permission +// Language. +func TestRewritesNamespaceConfig(t *testing.T) { + oplContent := ` +class User implements Namespace { + related: { + manager: User[] + } +} + +class Group implements Namespace { + related: { + members: (User | Group)[] + } +}` + + oplConfigFile := createFile(t, oplContent) + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(oplContent)) + if err != nil { + t.Fatal(err) + } + })) + t.Cleanup(func() { srv.Close() }) + + cases := []struct { + name string + location string + disallowPrivateIPRanges bool + expectErr bool + }{{ + name: "local file", + location: "file://" + oplConfigFile, + }, { + name: "HTTP url forbidden", + location: srv.URL, + disallowPrivateIPRanges: true, + expectErr: true, + }, { + name: "HTTP url allowed", + location: srv.URL, + }} + + for _, tc := range cases { + t.Run("case="+tc.name, func(t *testing.T) { + config := createFileF(t, ` +dsn: memory +clients: + http: + disallow_private_ip_ranges: %v +namespaces: + location: %s`, tc.disallowPrivateIPRanges, tc.location) + + _, p := setup(t, config) + nm, err := p.NamespaceManager() + if tc.expectErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + namespaces, err := nm.Namespaces(context.Background()) + require.NoError(t, err) + require.Len(t, namespaces, 2) + + names, relationNames := []string{namespaces[0].Name, namespaces[1].Name}, []string{namespaces[0].Relations[0].Name, namespaces[1].Relations[0].Name} + + assert.False(t, p.StrictMode()) + assert.ElementsMatch(t, names, []string{"User", "Group"}) + assert.ElementsMatch(t, relationNames, []string{"manager", "members"}) + }) + } + + for _, tt := range []struct { + name string + deprecated bool + new bool + want bool + }{ + {name: "neither key set", deprecated: false, new: false, want: false}, + {name: "deprecated key only", deprecated: true, new: false, want: true}, + {name: "new key only", deprecated: false, new: true, want: true}, + {name: "both keys true", deprecated: true, new: true, want: true}, + } { + t.Run("case=strict_mode/"+tt.name, func(t *testing.T) { + cfg := createFileF(t, ` +dsn: memory +namespaces: + location: file://%s + experimental_strict_mode: %v +feature_flags: + strict_mode: %v`, oplConfigFile, tt.deprecated, tt.new) + + _, p := setup(t, cfg) + assert.Equal(t, tt.want, p.StrictMode()) + }) + } +} + +func TestProvider_DefaultReadAPIListenOn(t *testing.T) { + ctx := context.Background() + config, err := NewDefault( + ctx, + pflag.NewFlagSet("test", pflag.ContinueOnError), + logrusx.New("", ""), + configx.WithValue("dsn", "foo"), + ) + require.NoError(t, err) + + addr, listenFile := config.ReadAPIListenOn() + assert.Equal(t, ":4466", addr) + assert.Zero(t, listenFile) +} diff --git a/internal/driver/daemon.go b/internal/driver/daemon.go new file mode 100644 index 0000000..c96519c --- /dev/null +++ b/internal/driver/daemon.go @@ -0,0 +1,570 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + stderrors "errors" + "fmt" + "net" + "net/http" + "os" + "os/signal" + "runtime/debug" + "strings" + "syscall" + "time" + + "connectrpc.com/connect" + "connectrpc.com/grpchealth" + "connectrpc.com/grpcreflect" + "github.com/ory/analytics-go/v5" + "github.com/ory/graceful" + "github.com/ory/herodot" + "github.com/ory/x/healthx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/logrusx" + "github.com/ory/x/metricsx" + "github.com/ory/x/otelx" + "github.com/ory/x/otelx/semconv" + "github.com/ory/x/prometheusx" + "github.com/ory/x/reqlog" + "github.com/ory/x/urlx" + "github.com/pkg/errors" + "github.com/rs/cors" + "github.com/spf13/cobra" + "github.com/urfave/negroni" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "golang.org/x/sync/errgroup" + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/protobuf/reflect/protoregistry" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/namespace/namespacehandler" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/schema" +) + +func (r *RegistryDefault) enableSqa(cmd *cobra.Command) { + ctx := cmd.Context() + + var urls []string + addr, _ := r.Config(ctx).ReadAPIListenOn() + urls = append(urls, addr) + addr, _ = r.Config(ctx).WriteAPIListenOn() + urls = append(urls, addr) + addr, _ = r.Config(ctx).MetricsListenOn() + urls = append(urls, addr) + addr, _ = r.Config(ctx).OPLSyntaxAPIListenOn() + urls = append(urls, addr) + + if c, y := r.Config(ctx).CORS("read"); y { + urls = append(urls, c.AllowedOrigins...) + } + if c, y := r.Config(ctx).CORS("write"); y { + urls = append(urls, c.AllowedOrigins...) + } + if c, y := r.Config(ctx).CORS("metrics"); y { + urls = append(urls, c.AllowedOrigins...) + } + + host := urlx.ExtractPublicAddress(urls...) + + r.sqaService = metricsx.New( + cmd, + r.Logger(), + r.Config(ctx).Source(), + &metricsx.Options{ + Service: "keto", + DeploymentId: metricsx.Hash(r.Persister().NetworkID(ctx).String()), + IsDevelopment: strings.HasPrefix(r.Config(ctx).DSN(), "sqlite"), + WriteKey: "jk32cFATnj9GKbQdFL7fBB9qtKZdX9j7", + WhitelistedPaths: []string{ + "/", + healthx.AliveCheckPath, + healthx.ReadyCheckPath, + healthx.VersionPath, + relationtuplesconnect.VersionServiceGetVersionProcedure, + "/" + grpchealth.HealthV1ServiceName + "/Check", + "/" + grpchealth.HealthV1ServiceName + "/Watch", + + relationtuple.ReadRouteBase, + relationtuplesconnect.ReadServiceListRelationTuplesProcedure, + check.RouteBase, + relationtuplesconnect.CheckServiceCheckProcedure, + relationtuplesconnect.CheckServiceBatchCheckProcedure, + expand.RouteBase, + relationtuplesconnect.ExpandServiceExpandProcedure, + relationtuplesconnect.WriteServiceTransactRelationTuplesProcedure, + relationtuplesconnect.WriteServiceDeleteRelationTuplesProcedure, + }, + BuildVersion: config.Version, + BuildHash: config.Commit, + BuildTime: config.Date, + Config: &analytics.Config{ + Endpoint: "https://sqa.ory.sh", + GzipCompressionLevel: 6, + BatchMaxSize: 500 * 1000, + BatchSize: 1000, + Interval: time.Hour * 6, + }, + Hostname: host, + }, + ) +} + +func (r *RegistryDefault) ServeAllSQA(cmd *cobra.Command) error { + r.enableSqa(cmd) + return r.ServeAll(cmd.Context()) +} + +func (r *RegistryDefault) ServeAll(ctx context.Context) error { + innerCtx, cancel := context.WithCancel(ctx) + defer cancel() + + serveFuncs := []func(context.Context, chan<- struct{}) func() error{ + r.serveRead, + r.serveWrite, + r.serveOPLSyntax, + r.serveMetrics, + } + + doneShutdown := make(chan struct{}, len(serveFuncs)) + + go func() { + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + + select { + case <-osSignals: + cancel() + case <-innerCtx.Done(): + } + + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), graceful.DefaultShutdownTimeout) + defer cancel() + + nWaitingForShutdown := len(serveFuncs) + for { + select { + case <-ctx.Done(): + return + case <-doneShutdown: + nWaitingForShutdown-- + if nWaitingForShutdown == 0 { + // graceful shutdown done + return + } + } + } + }() + + eg := &errgroup.Group{} + + // We need to separate the setup (invoking the functions that return the serve functions) from running the serve + // functions to mitigate race conditions in the HTTP router. + for _, serve := range serveFuncs { + eg.Go(serve(innerCtx, doneShutdown)) + } + + return eg.Wait() +} + +func (r *RegistryDefault) serveRead(ctx context.Context, done chan<- struct{}) func() error { + rt := r.ReadRouter(ctx) + + if tracer := r.Tracer(ctx); tracer.IsLoaded() { + rt = otelx.NewMiddleware(rt, "serveRead", + otelhttp.WithTracerProvider(tracer.Provider()), + ) + } + + return func() error { + addr, listenFile := r.Config(ctx).ReadAPIListenOn() + return servePort(ctx, r.Logger().WithField("endpoint", "read"), addr, listenFile, rt, done) + } +} + +func (r *RegistryDefault) serveWrite(ctx context.Context, done chan<- struct{}) func() error { + rt := r.WriteRouter(ctx) + + if tracer := r.Tracer(ctx); tracer.IsLoaded() { + rt = otelx.NewMiddleware(rt, "serveWrite", + otelhttp.WithTracerProvider(tracer.Provider()), + ) + } + + return func() error { + addr, listenFile := r.Config(ctx).WriteAPIListenOn() + return servePort(ctx, r.Logger().WithField("endpoint", "write"), addr, listenFile, rt, done) + } +} + +func (r *RegistryDefault) serveOPLSyntax(ctx context.Context, done chan<- struct{}) func() error { + rt := r.OPLSyntaxRouter(ctx) + + if tracer := r.Tracer(ctx); tracer.IsLoaded() { + rt = otelx.NewMiddleware(rt, "serveOPLSyntax", + otelhttp.WithTracerProvider(tracer.Provider()), + ) + } + + return func() error { + addr, listenFile := r.Config(ctx).OPLSyntaxAPIListenOn() + return servePort(ctx, r.Logger().WithField("endpoint", "opl"), addr, listenFile, rt, done) + } +} + +func (r *RegistryDefault) serveMetrics(ctx context.Context, done chan<- struct{}) func() error { + ctx, cancel := context.WithCancel(ctx) + + //nolint:gosec // graceful.WithDefaults already sets a timeout + s := graceful.WithDefaults(&http.Server{ + Handler: r.metricsRouter(ctx), + }) + + return func() error { + defer cancel() + eg := &errgroup.Group{} + + addr, listenFile := r.Config(ctx).MetricsListenOn() + l, err := listenAndWriteFile(ctx, addr, listenFile) + if err != nil { + return err + } + + eg.Go(func() error { + if err := s.Serve(l); !errors.Is(err, http.ErrServerClosed) { + return errors.WithStack(err) + } + return nil + }) + eg.Go(func() (err error) { + defer func() { + l := r.Logger().WithField("endpoint", "metrics") + if err != nil { + l.WithError(err).Error("graceful shutdown failed") + } else { + l.Info("gracefully shutdown server") + } + done <- struct{}{} + }() + + <-ctx.Done() + ctx, cancel := context.WithTimeout(context.Background(), graceful.DefaultShutdownTimeout) + defer cancel() + return s.Shutdown(ctx) + }) + + return eg.Wait() + } +} + +func servePort(ctx context.Context, log *logrusx.Logger, addr, listenFile string, router http.Handler, done chan<- struct{}) error { + l, err := listenAndWriteFile(ctx, addr, listenFile) + if err != nil { + return err + } + + p := new(http.Protocols) + p.SetHTTP1(true) + // For gRPC clients, it's convenient to support HTTP/2 without TLS. + p.SetUnencryptedHTTP2(true) + p.SetHTTP2(true) + //nolint:gosec // graceful.WithDefaults already sets a timeout + restS := graceful.WithDefaults(&http.Server{ + Handler: router, + Protocols: p, + }) + + eg := &errgroup.Group{} + + eg.Go(func() error { + if err := restS.Serve(l); !errors.Is(err, http.ErrServerClosed) { + return errors.WithStack(err) + } + return nil + }) + + eg.Go(func() (err error) { + defer func() { + if err != nil { + log.WithError(err).Error("graceful shutdown failed") + } else { + log.Info("gracefully shutdown server") + } + done <- struct{}{} + }() + + <-ctx.Done() + + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), graceful.DefaultShutdownTimeout) + defer cancel() + + shutdownEg := errgroup.Group{} + shutdownEg.Go(func() error { + // we ignore net.ErrClosed, because a cmux listener's close func is actually the one of the root listener (which is closed in a racy fashion) + if err := restS.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) { + // unexpected error + return errors.WithStack(err) + } + return nil + }) + shutdownEg.Go(func() error { + gracefulDone := make(chan struct{}) + go func() { + close(gracefulDone) + }() + select { + case <-gracefulDone: + return nil + case <-ctx.Done(): + return errors.New("graceful stop of gRPC server timed out, had to force it") + } + }) + + return shutdownEg.Wait() + }) + + return eg.Wait() +} + +func (r *RegistryDefault) allHandlers() []Handler { + return []Handler{ + relationtuple.NewReadHandler(r), + relationtuple.NewWriteHandler(r), + check.NewHandler(r), + expand.NewHandler(r), + namespacehandler.New(r), + schema.NewHandler(r), + } +} + +func listenAndWriteFile(ctx context.Context, addr, listenFile string) (net.Listener, error) { + l, err := (&net.ListenConfig{}).Listen(ctx, "tcp", addr) + if err != nil { + return nil, errors.WithStack(fmt.Errorf("unable to listen on %q: %w", addr, err)) + } + const filePrefix = "file://" + if strings.HasPrefix(listenFile, filePrefix) { + if err := os.WriteFile(listenFile[len(filePrefix):], []byte(l.Addr().String()), 0o600); err != nil { + return nil, errors.WithStack(fmt.Errorf("unable to write listen file %q: %w", listenFile, err)) + } + } + return l, nil +} + +var httpMetrics = prometheusx.NewHTTPMetrics("keto", prometheusx.HTTPPrefix, config.Version, config.Commit, config.Date) + +func (r *RegistryDefault) ReadRouter(ctx context.Context) http.Handler { + recovery := negroni.NewRecovery() + recovery.Logger = r.Logger() + + router := httprouterx.NewRouterPublic() + n := negroni.New( + recovery, + httpMetrics, + ) + for _, f := range r.defaultHttpMiddlewares { + n.UseFunc(f) + } + n.UseFunc(semconv.Middleware) + n.Use(reqlog.NewMiddlewareFromLogger(r.l, "read#Ory Keto").ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)) + + prometheusx.SetMuxRoutes(router) + r.HealthHandler().SetHealthRoutes(router, false) + r.HealthHandler().SetVersionRoutes(router) + + for _, h := range registerReflectionAndHealth[ReadHandler](r, router) { + h.RegisterReadRoutes(router) + } + + if r.sqaService != nil { + n.Use(r.sqaService) + } + + n.UseHandler(router) + + var handler http.Handler = n + options, enabled := r.Config(ctx).CORS("read") + if enabled { + handler = cors.New(options).Handler(handler) + } + + return handler +} + +func (r *RegistryDefault) WriteRouter(ctx context.Context) http.Handler { + recovery := negroni.NewRecovery() + recovery.Logger = r.Logger() + + router := httprouterx.NewRouterAdmin() + n := negroni.New( + recovery, + httpMetrics, + ) + for _, f := range r.defaultHttpMiddlewares { + n.UseFunc(f) + } + n.UseFunc(semconv.Middleware) + n.Use(reqlog.NewMiddlewareFromLogger(r.l, "write#Ory Keto").ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)) + + prometheusx.SetMuxRoutes(router) + + r.HealthHandler().SetHealthRoutes(router, false) + r.HealthHandler().SetVersionRoutes(router) + + for _, h := range registerReflectionAndHealth[WriteHandler](r, router) { + h.RegisterWriteRoutes(router) + } + + if r.sqaService != nil { + n.Use(r.sqaService) + } + + n.UseHandler(router) + + var handler http.Handler = n + options, enabled := r.Config(ctx).CORS("write") + if enabled { + handler = cors.New(options).Handler(handler) + } + + return handler +} + +func (r *RegistryDefault) OPLSyntaxRouter(ctx context.Context) http.Handler { + recovery := negroni.NewRecovery() + recovery.Logger = r.Logger() + + router := httprouterx.NewRouter() + n := negroni.New( + recovery, + httpMetrics, + ) + for _, f := range r.defaultHttpMiddlewares { + n.UseFunc(f) + } + n.UseFunc(semconv.Middleware) + n.Use(reqlog.NewMiddlewareFromLogger(r.l, "syntax#Ory Keto").ExcludePaths(healthx.AliveCheckPath, healthx.ReadyCheckPath)) + + prometheusx.SetMuxRoutes(router) + + r.HealthHandler().SetHealthRoutes(router, false) + r.HealthHandler().SetVersionRoutes(router) + + for _, h := range registerReflectionAndHealth[OPLSyntaxHandler](r, router) { + h.RegisterSyntaxRoutes(router) + } + + if r.sqaService != nil { + n.Use(r.sqaService) + } + + n.UseHandler(router) + + var handler http.Handler = n + options, enabled := r.Config(ctx).CORS("write") + if enabled { + handler = cors.New(options).Handler(handler) + } + + return handler +} + +func (r *RegistryDefault) recoveryHandler(_ context.Context, spec connect.Spec, _ http.Header, val any) error { + r.Logger(). + WithField("reason", val). + WithField("procedure", spec.Procedure). + WithField("stack_trace", string(debug.Stack())). + Error("panic recovered") + return connect.NewError(connect.CodeInternal, fmt.Errorf("%v", val)) +} + +func (r *RegistryDefault) HandlerOptions() []connect.HandlerOption { + return append([]connect.HandlerOption{ + connect.WithRecover(r.recoveryHandler), + connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + resp, err := next(ctx, req) + if hErr, ok := stderrors.AsType[*herodot.DefaultError](err); ok { + cErr := connect.NewError(connect.Code(hErr.GRPCCodeField), hErr) + if reason := hErr.Reason(); reason != "" { + d, err := connect.NewErrorDetail(&errdetails.ErrorInfo{Reason: reason}) + if err == nil { + cErr.AddDetail(d) + } + } + return resp, cErr + } + return resp, err + } + })), + connect.WithInterceptors(r.defaultConnectInterceptors...), + }, + r.defaultHandlerOptions...) +} + +func (r *RegistryDefault) metricsRouter(ctx context.Context) http.Handler { + recovery := negroni.NewRecovery() + recovery.Logger = r.Logger() + + n := negroni.New( + recovery, + reqlog.NewMiddlewareFromLogger(r.Logger(), "keto").ExcludePaths(prometheusx.MetricsPrometheusPath), + ) + + router := http.NewServeMux() + prometheusx.SetMuxRoutes(router) + n.UseHandler(router) + + var handler http.Handler = n + options, enabled := r.Config(ctx).CORS("metrics") + if enabled { + handler = cors.New(options).Handler(handler) + } + return handler +} + +func registerReflectionAndHealth[H Handler](r *RegistryDefault, router httprouterx.Router) (handlers []H) { + protoFiles := &protoregistry.Files{} + var protoServices []string + for _, h := range r.allHandlers() { + if h, ok := h.(H); ok { + handlers = append(handlers, h) + for _, f := range h.ProtoFiles() { + if err := protoFiles.RegisterFile(f); err != nil && !strings.Contains(err.Error(), "is already registered") { + r.Logger().WithError(err). + WithField("file_path", f.Path()). + Error("unable to register proto file for reflection") + } + s := f.Services() + for i := range s.Len() { + protoServices = append(protoServices, string(s.Get(i).FullName())) + } + } + } + } + + router.Handle(relationtuplesconnect.NewVersionServiceHandler(r, r.HandlerOptions()...)) + protoServices = append(protoServices, relationtuplesconnect.VersionServiceName) + if err := protoFiles.RegisterFile(rts.File_ory_keto_relation_tuples_v1alpha2_version_proto); err != nil { + r.Logger().WithError(err).Error("unable to register version proto file for reflection") + } + + reflector := grpcreflect.NewReflector( + grpcreflect.NamerFunc(func() []string { return protoServices }), + grpcreflect.WithDescriptorResolver(protoFiles), + ) + router.Handle(grpcreflect.NewHandlerV1(reflector, r.HandlerOptions()...)) + router.Handle(grpcreflect.NewHandlerV1Alpha(reflector, r.HandlerOptions()...)) + + router.Handle(grpchealth.NewHandler(grpchealth.NewStaticChecker(protoServices...), r.HandlerOptions()...)) + + return +} diff --git a/internal/driver/daemon_test.go b/internal/driver/daemon_test.go new file mode 100644 index 0000000..8c694e1 --- /dev/null +++ b/internal/driver/daemon_test.go @@ -0,0 +1,182 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + "time" + + "connectrpc.com/connect" + ioprometheusclient "github.com/prometheus/client_model/go" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + + prometheus "github.com/ory/x/prometheusx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func TestScrapingEndpoint(t *testing.T) { + t.Parallel() + + r := NewSqliteTestRegistry(t) + getAddr := UseDynamicPorts(t, r) + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + eg := errgroup.Group{} + doneShutdown := make(chan struct{}) + eg.Go(r.serveRead(ctx, doneShutdown)) + eg.Go(r.serveMetrics(ctx, doneShutdown)) + + _, readPort, _ := getAddr(t, "read") + _, metricsPort, _ := getAddr(t, "metrics") + + t.Logf("write port: %s, metrics port: %s", readPort, metricsPort) + + assert.EventuallyWithT(t, func(t *assert.CollectT) { + conn, err := grpc.NewClient(fmt.Sprintf("127.0.0.1:%s", readPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { require.NoError(t, conn.Close()) }() + + cl := grpcHealthV1.NewHealthClient(conn) + resp, err := cl.Check(ctx, &grpcHealthV1.HealthCheckRequest{}) + require.NoError(t, err) + assert.Equal(t, grpcHealthV1.HealthCheckResponse_SERVING, resp.Status) + }, 2*time.Second, 10*time.Millisecond) + + conn, err := grpc.NewClient(fmt.Sprintf("127.0.0.1:%s", readPort), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { require.NoError(t, conn.Close()) }() + + cl := rts.NewReadServiceClient(conn) + resp, err := cl.ListRelationTuples(ctx, &rts.ListRelationTuplesRequest{RelationQuery: &rts.RelationQuery{}}) + require.NoError(t, err) + require.Len(t, resp.RelationTuples, 0) + + // A dedicated transport keeps httptest.Server.Close in parallel tests from + // breaking these requests via http.DefaultTransport.CloseIdleConnections. + transport := http.DefaultTransport.(*http.Transport).Clone() + t.Cleanup(transport.CloseIdleConnections) + hc := &http.Client{Transport: transport} + + respx, err := hc.Get(fmt.Sprintf("http://127.0.0.1:%s/relation-tuples", readPort)) + require.NoError(t, err) + t.Cleanup(func() { _ = respx.Body.Close() }) + require.EqualValues(t, http.StatusOK, respx.StatusCode) + body, err := io.ReadAll(respx.Body) + require.NoError(t, err) + require.Contains(t, string(body), `"relation_tuples":[]`) + + promresp, err := hc.Get(fmt.Sprintf("http://127.0.0.1:%s%s", metricsPort, prometheus.MetricsPrometheusPath)) + require.NoError(t, err) + t.Cleanup(func() { _ = promresp.Body.Close() }) + require.EqualValues(t, http.StatusOK, promresp.StatusCode) + promrespBody, err := io.ReadAll(promresp.Body) + require.NoError(t, err) + + textParser := expfmt.NewTextParser(model.UTF8Validation) + text, err := textParser.TextToMetricFamilies(bytes.NewReader(promrespBody)) + require.NoError(t, err) + + require.EqualValuesf(t, "http_requests_total", *text["http_requests_total"].Name, "%s", promrespBody) + require.ElementsMatch(t, + []string{"/grpc.health.v1.Health/", "/ory.keto.relation_tuples.v1alpha2.ReadService/ListRelationTuples", "/relation-tuples"}, + getLabelValues("endpoint", text["http_requests_total"].Metric), + ) + + cancel() + <-doneShutdown + <-doneShutdown + require.NoError(t, eg.Wait()) +} + +type panicInterceptor struct{} + +func (panicInterceptor) WrapUnary(connect.UnaryFunc) connect.UnaryFunc { + return func(context.Context, connect.AnyRequest) (connect.AnyResponse, error) { panic("test panic") } +} +func (panicInterceptor) WrapStreamingClient(connect.StreamingClientFunc) connect.StreamingClientFunc { + return func(context.Context, connect.Spec) connect.StreamingClientConn { panic("test panic") } +} +func (panicInterceptor) WrapStreamingHandler(connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return func(context.Context, connect.StreamingHandlerConn) error { panic("test panic") } +} + +var _ connect.Interceptor = (*panicInterceptor)(nil) + +func TestPanicRecovery(t *testing.T) { + t.Parallel() + + r := NewSqliteTestRegistry(t, WithHandlerOptions(connect.WithInterceptors(panicInterceptor{}))) + getAddr := UseDynamicPorts(t, r) + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + eg := errgroup.Group{} + doneShutdown := make(chan struct{}) + eg.Go(r.serveWrite(ctx, doneShutdown)) + + _, port, _ := getAddr(t, "write") + + conn, err := grpc.NewClient(fmt.Sprintf("127.0.0.1:%s", port), grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + defer func() { require.NoError(t, conn.Close()) }() + + assert.EventuallyWithT(t, func(t *assert.CollectT) { + cl := grpcHealthV1.NewHealthClient(conn) + + watcher, err := cl.Watch(ctx, &grpcHealthV1.HealthCheckRequest{}) + require.NoError(t, err) + require.NoError(t, watcher.CloseSend()) + for err := status.Error(codes.Unavailable, "init"); status.Code(err) != codes.Unavailable; _, err = watcher.Recv() { + } + }, 2*time.Second, 10*time.Millisecond) + + cl := grpcHealthV1.NewHealthClient(conn) + // we want to ensure the server is still running after the panic + for range 10 { + // Unary call + resp, err := cl.Check(ctx, &grpcHealthV1.HealthCheckRequest{}) + require.Errorf(t, err, "%+v", resp) + assert.Equal(t, codes.Internal, status.Code(err)) + + // Streaming call + wResp, err := cl.Watch(ctx, &grpcHealthV1.HealthCheckRequest{}) + require.NoError(t, err) + err = wResp.RecvMsg(nil) + require.Error(t, err) + assert.Equal(t, codes.Internal, status.Code(err)) + } + + cancel() + <-doneShutdown + require.NoError(t, eg.Wait()) +} + +func getLabelValues(name string, metric []*ioprometheusclient.Metric) (vals []string) { + for _, m := range metric { + for _, label := range m.Label { + if *label.Name == name { + vals = append(vals, *label.Value) + } + } + } + return vals +} diff --git a/internal/driver/logrus_interceptor.go b/internal/driver/logrus_interceptor.go new file mode 100644 index 0000000..e9b964a --- /dev/null +++ b/internal/driver/logrus_interceptor.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + "github.com/sirupsen/logrus" +) + +// InterceptorLogger adapts logrus logger to interceptor logger. +// This code is simple enough to be copied and not imported. +// +// Source from: https://github.com/grpc-ecosystem/go-grpc-middleware/blob/main/interceptors/logging/examples/logrus/example_test.go +func InterceptorLogger(l logrus.FieldLogger) logging.Logger { + return logging.LoggerFunc(func(_ context.Context, lvl logging.Level, msg string, fields ...any) { + f := make(map[string]any, len(fields)/2) + i := logging.Fields(fields).Iterator() + if i.Next() { + k, v := i.At() + f[k] = v + } + l := l.WithFields(f) + + switch lvl { + case logging.LevelDebug: + l.Debug(msg) + case logging.LevelInfo: + l.Info(msg) + case logging.LevelWarn: + l.Warn(msg) + case logging.LevelError: + l.Error(msg) + default: + l.Info(msg) + } + }) +} diff --git a/internal/driver/pop_connection.go b/internal/driver/pop_connection.go new file mode 100644 index 0000000..5ac5fc6 --- /dev/null +++ b/internal/driver/pop_connection.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "time" + + "github.com/cenkalti/backoff/v3" + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/sqlcon" +) + +func (r *RegistryDefault) PopConnectionWithOpts(ctx context.Context, popOpts ...func(*pop.ConnectionDetails)) (*pop.Connection, error) { + pool, idlePool, connMaxLifetime, connMaxIdleTime, cleanedDSN := sqlcon.ParseConnectionOptions(r.Logger(), r.Config(ctx).DSN()) + connDetails := &pop.ConnectionDetails{ + URL: sqlcon.FinalizeDSN(r.Logger(), cleanedDSN), + IdlePool: idlePool, + ConnMaxLifetime: connMaxLifetime, + ConnMaxIdleTime: connMaxIdleTime, + Pool: pool, + TracerProvider: r.Tracer(ctx).Provider(), + } + for _, o := range popOpts { + o(connDetails) + } + + bc := backoff.NewExponentialBackOff() + bc.MaxElapsedTime = time.Minute * 5 + bc.Reset() + + var conn *pop.Connection + if err := backoff.Retry(func() (err error) { + conn, err = pop.NewConnection(connDetails) + if err != nil { + r.Logger().WithError(err).Error("Unable to connect to database, retrying.") + return errors.WithStack(err) + } + + if err := conn.Open(); err != nil { + r.Logger().WithError(err).Error("Unable to open the database connection, retrying.") + return errors.WithStack(err) + } + + if err := conn.Store.(interface{ Ping() error }).Ping(); err != nil { + r.Logger().WithError(err).Error("Unable to ping the database connection, retrying.") + return errors.WithStack(err) + } + + return nil + }, bc); err != nil { + return nil, errors.WithStack(err) + } + + // Wrap the connection with the context before starting the close + // goroutine. WithContext reads conn.Store (via copy), and conn.Close sets + // conn.Store to nil; if the context is already canceled, the goroutine + // would otherwise race the read. Ordering the read first makes it + // happen-before the goroutine starts. + wrapped := conn.WithContext(ctx) + + // Close this connection when the context is closed. + go func() { + <-ctx.Done() + if err := conn.Close(); err != nil { + r.Logger().WithError(err).Error("Unable to close database connection") + } + }() + + return wrapped, nil +} + +// PopConnection returns the standard connection that is kept for the whole time. +func (r *RegistryDefault) PopConnection(ctx context.Context) (*pop.Connection, error) { + if r.conn == nil { + var err error + r.conn, err = r.PopConnectionWithOpts(ctx, r.dbOpts...) + return r.conn, err + } + return r.conn, nil +} diff --git a/internal/driver/registry.go b/internal/driver/registry.go new file mode 100644 index 0000000..ea2cd70 --- /dev/null +++ b/internal/driver/registry.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "net/http" + + "connectrpc.com/connect" + "github.com/ory/pop/v6" + "github.com/ory/x/healthx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/spf13/cobra" + + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/persistence" + "github.com/ory/keto/internal/relationtuple" +) + +type ( + Registry interface { + Init(context.Context) error + + config.Provider + logrusx.Provider + httpx.WriterProvider + + relationtuple.ManagerProvider + relationtuple.MapperProvider + expand.EngineProvider + check.CheckerProvider + check.EngineProvider + persistence.Migrator + persistence.Provider + + PopConnection(ctx context.Context) (*pop.Connection, error) + PopConnectionWithOpts(ctx context.Context, f ...func(*pop.ConnectionDetails)) (*pop.Connection, error) + + HealthHandler() *healthx.Handler + Tracer(ctx context.Context) *otelx.Tracer + + ReadRouter(ctx context.Context) http.Handler + WriteRouter(ctx context.Context) http.Handler + OPLSyntaxRouter(ctx context.Context) http.Handler + + ServeAll(ctx context.Context) error + ServeAllSQA(cmd *cobra.Command) error + + HandlerOptions() []connect.HandlerOption + } + + contextKeys string +) + +const ( + LogrusHookContextKey contextKeys = "logrus hook" + RegistryContextKey contextKeys = "registry" +) diff --git a/internal/driver/registry_default.go b/internal/driver/registry_default.go new file mode 100644 index 0000000..8a35541 --- /dev/null +++ b/internal/driver/registry_default.go @@ -0,0 +1,392 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "io/fs" + "maps" + "net/http" + "sync" + + "connectrpc.com/connect" + "github.com/gofrs/uuid" + "github.com/ory/herodot" + "github.com/pkg/errors" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/health" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/pop/v6" + "github.com/ory/x/contextx" + "github.com/ory/x/dbal" + "github.com/ory/x/fsx" + "github.com/ory/x/healthx" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/metricsx" + "github.com/ory/x/networkx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/check/step" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/persistence" + "github.com/ory/keto/internal/persistence/sql" + "github.com/ory/keto/internal/persistence/sql/migrations/uuidmapping" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoctx" +) + +var ( + _ relationtuple.ManagerProvider = (*RegistryDefault)(nil) + _ relationtuple.MapperProvider = (*RegistryDefault)(nil) + _ relationtuple.MappingManagerProvider = (*RegistryDefault)(nil) + _ httpx.WriterProvider = (*RegistryDefault)(nil) + _ logrusx.Provider = (*RegistryDefault)(nil) + _ Registry = (*RegistryDefault)(nil) + _ relationtuplesconnect.VersionServiceHandler = (*RegistryDefault)(nil) +) + +type ( + RegistryDefault struct { + relationtuplesconnect.UnimplementedVersionServiceHandler + p persistence.Persister + mb *popx.MigrationBox + extraMigrations []fs.FS + l *logrusx.Logger + w herodot.Writer + ce *check.Engine + ck *step.Executor + ee *expand.Engine + c *config.Config + conn *pop.Connection + ctxer contextx.Contextualizer + mapper *relationtuple.Mapper + readOnlyMapper *relationtuple.Mapper + replaceMapperNs func(ctx context.Context) uuid.UUID + replaceShardID func() uuid.UUID + + init1, init2 sync.Once + init1err, init2err error + + healthH *healthx.Handler + healthServer *health.Server + sqaService *metricsx.Service + tracer *otelx.Tracer + tracerWrapper ketoctx.TracerWrapper + + defaultHandlerOptions []connect.HandlerOption + defaultConnectInterceptors []connect.Interceptor + defaultHttpMiddlewares []func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) + grpcTransportCredentials credentials.TransportCredentials + defaultMigrationOptions []popx.MigrationBoxOption + healthReadyCheckers healthx.ReadyCheckers + dbOpts []func(details *pop.ConnectionDetails) + } + ReadHandler interface { + Handler + RegisterReadRoutes(r *httprouterx.RouterPublic) + } + WriteHandler interface { + Handler + RegisterWriteRoutes(r *httprouterx.RouterAdmin) + } + OPLSyntaxHandler interface { + Handler + RegisterSyntaxRoutes(r httprouterx.Router) + } + Handler interface { + ProtoFiles() []protoreflect.FileDescriptor + } +) + +func (r *RegistryDefault) Mapper() *relationtuple.Mapper { + if r.mapper == nil { + r.mapper = &relationtuple.Mapper{D: r} + } + return r.mapper +} + +func (r *RegistryDefault) ReadOnlyMapper() *relationtuple.Mapper { + if r.readOnlyMapper == nil { + r.readOnlyMapper = &relationtuple.Mapper{D: r, ReadOnly: true} + } + return r.readOnlyMapper +} + +func (r *RegistryDefault) Contextualizer() contextx.Contextualizer { + return r.ctxer +} + +func (r *RegistryDefault) Config(ctx context.Context) *config.Config { + if provider := r.ctxer.Config(ctx, r.c.Source()); provider != r.c.Source() { + return config.New(ctx, r.Logger(), provider) + } + return r.c +} + +func (r *RegistryDefault) HealthHandler() *healthx.Handler { + if r.healthH != nil { + return r.healthH + } + + h := healthx.ReadyCheckers{ + "database": func(req *http.Request) error { + return r.Persister().Connection(req.Context()).Store.PingContext(req.Context()) + }, + "migrations": func(req *http.Request) error { + mb, err := r.MigrationBox(req.Context()) + if err != nil { + return err + } + status, err := mb.Status(req.Context()) + if err != nil { + return err + } + + if status.HasPending() { + return errors.Errorf("migrations have not yet been fully applied") + } + return nil + }, + } + maps.Copy(h, r.healthReadyCheckers) // possibly override default checkers + + r.healthH = healthx.NewHandler(r.Writer(), config.Version, h) + + return r.healthH +} + +func (r *RegistryDefault) HealthServer() *health.Server { + if r.healthServer == nil { + r.healthServer = health.NewServer() + } + + return r.healthServer +} + +func (r *RegistryDefault) GetVersion(_ context.Context, _ *connect.Request[rts.GetVersionRequest]) (*connect.Response[rts.GetVersionResponse], error) { + return connect.NewResponse(&rts.GetVersionResponse{Version: config.Version}), nil +} + +func (r *RegistryDefault) Tracer(ctx context.Context) *otelx.Tracer { + if r.tracer == nil { + // Tracing is initialized only once, so it can not be hot reloaded or context-aware. + t, err := otelx.New("Ory Keto", r.Logger(), r.Config(ctx).TracingConfig()) + if err != nil { + r.Logger().WithError(err).Fatalf("Unable to initialize Tracer.") + } + + // Wrap the tracer if required + if r.tracerWrapper != nil { + t = r.tracerWrapper(t) + } + + r.tracer = t + } + + return r.tracer +} + +func (r *RegistryDefault) Logger() *logrusx.Logger { + if r.l == nil { + r.l = logrusx.New("Ory Keto", config.Version) + } + return r.l +} + +func (r *RegistryDefault) Writer() herodot.Writer { + if r.w == nil { + r.w = herodot.NewJSONWriter(r.Logger()) + } + return r.w +} + +func (r *RegistryDefault) RelationTupleManager() relationtuple.Manager { + if r.p == nil { + panic("no relation tuple manager, but expected to have one") + } + return r.p +} + +func (r *RegistryDefault) MappingManager() relationtuple.MappingManager { + if r.p == nil { + panic("no relation tuple manager, but expected to have one") + } + return r.p +} + +func (r *RegistryDefault) Persister() persistence.Persister { + if r.p == nil { + panic("no persister, but expected to have one") + } + return r.p +} + +func (r *RegistryDefault) NetworkID(ctx context.Context) uuid.UUID { + if r.p == nil { + panic("no persister, but expected to have one") + } + return r.p.NetworkID(ctx) +} + +func (r *RegistryDefault) NewShardID() uuid.UUID { + if r.replaceShardID != nil { + return r.replaceShardID() + } + return uuid.Must(uuid.NewV4()) +} + +func (r *RegistryDefault) MapperNamespace(ctx context.Context) uuid.UUID { + if r.replaceMapperNs != nil { + return r.replaceMapperNs(ctx) + } + return r.p.NetworkID(ctx) +} + +func (r *RegistryDefault) PermissionEngine() *check.Engine { + if r.ce == nil { + r.ce = check.NewEngine(r) + } + return r.ce +} + +func (r *RegistryDefault) ExpandEngine() *expand.Engine { + if r.ee == nil { + r.ee = expand.NewEngine(r) + } + return r.ee +} + +func (r *RegistryDefault) Checker() check.Checker { + if r.ck == nil { + r.ck = step.NewExecutor(r, step.CallLimitMiddleware()) + } + return r.ck +} + +func (r *RegistryDefault) MigrationBox(ctx context.Context, opts ...popx.MigrationBoxOption) (*popx.MigrationBox, error) { + if r.mb == nil { + c, err := r.PopConnection(ctx) + if err != nil { + return nil, err + } + namespaces, err := r.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + migrationBoxOptions := append(opts, + popx.WithGoMigrations(uuidmapping.Migrations(namespaces))) + + migrationBoxOptions = append(migrationBoxOptions, r.defaultMigrationOptions...) + + mb, err := popx.NewMigrationBox( + fsx.Merge(append([]fs.FS{sql.Migrations, networkx.Migrations}, r.extraMigrations...)...), + c, r.Logger(), + migrationBoxOptions..., + ) + if err != nil { + return nil, err + } + + r.mb = mb + } + return r.mb, nil +} + +func (r *RegistryDefault) MigrateUp(ctx context.Context) error { + mb, err := r.MigrationBox(ctx) + if err != nil { + return err + } + if err := mb.Up(ctx); err != nil { + return err + } + return r.Init(ctx) +} + +func (r *RegistryDefault) MigrateDown(ctx context.Context) error { + mb, err := r.MigrationBox(ctx) + if err != nil { + return err + } + return mb.Down(ctx, -1) +} + +func (r *RegistryDefault) DetermineNetwork(ctx context.Context) (*networkx.Network, error) { + c, err := r.PopConnection(ctx) + if err != nil { + return nil, err + } + mb, err := popx.NewMigrationBox(networkx.Migrations, c, r.Logger()) + if err != nil { + return nil, err + } + s, err := mb.Status(ctx) + if err != nil { + return nil, err + } + if s.HasPending() { + return nil, errors.WithStack(persistence.ErrNetworkMigrationsMissing) + } + + return networkx.Determine(c.WithContext(ctx)) +} + +func (r *RegistryDefault) InitWithoutNetworkID(ctx context.Context) error { + r.init1.Do(func() { + if dbal.IsMemorySQLite(r.Config(ctx).DSN()) { + mb, err := r.MigrationBox(ctx) + if err != nil { + r.init1err = err + return + } + + if err := mb.Up(ctx); err != nil { + r.init1err = err + return + } + } + + p, err := sql.NewPersister(ctx, r, uuid.Nil) + if err != nil { + r.init1err = err + return + } + r.p = p + r.initEngines() + }) + return r.init1err +} + +func (r *RegistryDefault) initEngines() { + _ = r.PermissionEngine() + _ = r.ExpandEngine() + _ = r.Checker() +} + +func (r *RegistryDefault) Init(ctx context.Context) (err error) { + r.init2.Do(func() { + if err := r.InitWithoutNetworkID(ctx); err != nil { + r.init2err = err + return + } + + network, err := r.DetermineNetwork(ctx) + if err != nil { + r.init2err = err + return + } + + r.p.SetNetwork(network.ID) + }) + return r.init2err +} diff --git a/internal/driver/registry_factory.go b/internal/driver/registry_factory.go new file mode 100644 index 0000000..cc282e4 --- /dev/null +++ b/internal/driver/registry_factory.go @@ -0,0 +1,242 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "fmt" + "os" + "sync" + "testing" + + "connectrpc.com/connect" + "github.com/gofrs/uuid" + "github.com/ory/x/contextx" + + "github.com/ory/x/configx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/tlsx" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/credentials" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/x/dbx" + "github.com/ory/keto/ketoctx" +) + +// createFile writes the content to a temporary file, returning the path. +// Good for testing config files. +func createFile(t testing.TB, content string) (path string) { + t.Helper() + + f, err := os.CreateTemp(t.TempDir(), "config-*.yaml") + if err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { _ = os.Remove(f.Name()) }) //nolint:gosec // Test file path from os.CreateTemp, not user input. + + n, err := f.WriteString(content) + if err != nil { + t.Fatal(err) + } + if n != len(content) { + t.Fatal("failed to write the complete content") + } + + return f.Name() +} + +func NewDefaultRegistry(ctx context.Context, flags *pflag.FlagSet, withoutNetwork bool, opts []ketoctx.Option) (Registry, error) { + reg, ok := ctx.Value(RegistryContextKey).(Registry) + if ok { + return reg, nil + } + + options := ketoctx.Options(opts...) + + l := options.Logger() + if l == nil { + l = newLogger(ctx) + } + + c := config.New(ctx, l, nil) + cp, err := config.NewProvider(ctx, flags, c) + if err != nil { + return nil, errors.Wrap(err, "unable to initialize config provider") + } + c.WithSource(options.Contextualizer().Config(ctx, cp)) + + r := &RegistryDefault{ + c: c, + l: l, + tracerWrapper: options.TracerWrapper, + ctxer: options.Contextualizer(), + defaultConnectInterceptors: options.ConnectInterceptors(), + defaultHttpMiddlewares: options.HTTPMiddlewares(), + extraMigrations: options.ExtraMigrations(), + defaultMigrationOptions: options.MigrationOptions(), + healthReadyCheckers: options.ReadyCheckers(), + dbOpts: options.DBOptionsModifiers(), + } + + init := r.Init + if withoutNetwork { + init = r.InitWithoutNetworkID + } + if err := init(ctx); err != nil { + return nil, errors.Wrap(err, "unable to initialize service registry") + } + + if inspect := options.Inspect(); inspect != nil { + if err := inspect(r.Persister().Connection(ctx)); err != nil { + return nil, errors.Wrap(err, "inspect") + } + } + + return r, nil +} + +func NewSqliteTestRegistry(t testing.TB, opts ...TestRegistryOption) *RegistryDefault { + return NewTestRegistry(t, dbx.GetSqlite(t), opts...) +} + +type TestRegistryOption func(t testing.TB, r *RegistryDefault) + +func WithConfig(key string, value any) TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + require.NoError(t, r.c.Set(key, value)) + } +} + +func WithMapperNamespace(f func(ctx context.Context) uuid.UUID) TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + r.replaceMapperNs = f + } +} + +// WithOrderedShardID switches shard ID generation to UUID v7 so that inserts +// are monotonically ordered and predictable in tests. +func WithOrderedShardID() TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + r.replaceShardID = func() uuid.UUID { return uuid.Must(uuid.NewV7()) } + } +} + +func WithNamespaces(namespaces []*namespace.Namespace) TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + require.NoError(t, r.c.Set(config.KeyNamespaces, namespaces)) + } +} + +func WithOPL(opl string) TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + f := createFile(t, opl) + require.NoError(t, r.c.Set(config.KeyNamespaces+".location", "file://"+f)) + } +} + +func WithHandlerOptions(opts ...connect.HandlerOption) TestRegistryOption { + return func(_ testing.TB, r *RegistryDefault) { + r.defaultHandlerOptions = append(r.defaultHandlerOptions, opts...) + } +} + +func WithTracer(tracer trace.Tracer) TestRegistryOption { + return func(_ testing.TB, r *RegistryDefault) { + r.tracer = new(otelx.Tracer).WithOTLP(tracer) + } +} + +func WithLogLevel(level string) TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + require.NoError(t, r.c.Set("log.level", level)) + } +} + +type selfSignedCert struct { + once sync.Once + cert *tls.Certificate + err error +} + +var sharedTestCert selfSignedCert + +func (s *selfSignedCert) generate() { + s.once.Do(func() { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + s.err = fmt.Errorf("could not create key: %v", err) + return + } + + s.cert, err = tlsx.CreateSelfSignedTLSCertificate(key) + if err != nil { + s.err = fmt.Errorf("could not create TLS certificate: %v", err) + } + }) +} + +func WithSelfSignedTransportCredentials() TestRegistryOption { + return func(t testing.TB, r *RegistryDefault) { + sharedTestCert.generate() + require.NoError(t, sharedTestCert.err) + + r.grpcTransportCredentials = credentials.NewServerTLSFromCert(sharedTestCert.cert) + } +} + +func NewTestRegistry(t testing.TB, dsn *dbx.DsnT, opts ...TestRegistryOption) *RegistryDefault { + l := logrusx.New("Ory Keto", "testing") + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + ctx = configx.ContextWithConfigOptions(ctx, configx.WithValues(map[string]interface{}{ + config.KeyDSN: dsn.Conn, + "log.level": "info", + config.KeyNamespaces: []*namespace.Namespace{}, + config.KeySecretsPagination: []string{uuid.Must(uuid.NewV4()).String()}, + })) + c, err := config.NewDefault(ctx, nil, l) + require.NoError(t, err) + + r := &RegistryDefault{ + c: c, + l: l, + ctxer: &contextx.Default{}, + } + + for _, opt := range opts { + opt(t, r) + } + + if dsn.MigrateUp { + require.NoError(t, r.MigrateUp(ctx)) + } + + require.NoError(t, r.Init(ctx)) + + return r +} + +func newLogger(ctx context.Context) *logrusx.Logger { + hook, ok := ctx.Value(LogrusHookContextKey).(logrus.Hook) + + var opts []logrusx.Option + if ok { + opts = append(opts, logrusx.WithHook(hook)) + } + + return logrusx.New("Ory Keto", config.Version, opts...) +} diff --git a/internal/driver/selfsigned_cert_bench_test.go b/internal/driver/selfsigned_cert_bench_test.go new file mode 100644 index 0000000..91ff1a3 --- /dev/null +++ b/internal/driver/selfsigned_cert_bench_test.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package driver + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/ory/x/tlsx" +) + +func BenchmarkCertificateGeneration(b *testing.B) { + cases := []struct { + name string + curve elliptic.Curve + }{ + {"P256", elliptic.P256()}, + {"P224", elliptic.P224()}, + {"P384", elliptic.P384()}, + {"P521", elliptic.P521()}, + } + + for _, tc := range cases { + tc := tc + b.Run(tc.name, func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + key, err := ecdsa.GenerateKey(tc.curve, rand.Reader) + if err != nil { + b.Fatalf("could not create key: %v", err) + } + if _, err = tlsx.CreateSelfSignedTLSCertificate(key); err != nil { + b.Fatalf("could not create TLS certificate: %v", err) + } + } + }) + } +} diff --git a/internal/driver/testhelpers.go b/internal/driver/testhelpers.go new file mode 100644 index 0000000..35caf5d --- /dev/null +++ b/internal/driver/testhelpers.go @@ -0,0 +1,84 @@ +package driver + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/credentials" + + "github.com/ory/keto/internal/driver/config" +) + +type GetAddr = func(t testing.TB, endpoint string) (host string, port string, fullAddr string) + +func UseDynamicPorts(t testing.TB, r Registry) GetAddr { + t.Helper() + + listenDir := t.TempDir() + readListenFile := fmt.Sprintf("%s/read.addr", listenDir) + writeListenFile := fmt.Sprintf("%s/write.addr", listenDir) + metricsListenFile := fmt.Sprintf("%s/metrics.addr", listenDir) + oplListenFile := fmt.Sprintf("%s/opl.addr", listenDir) + + require.NoError(t, r.Config(t.Context()).Set(config.KeyReadAPIPort, 0)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyReadAPIListenFile, "file://"+readListenFile)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyWriteAPIPort, 0)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyWriteAPIListenFile, "file://"+writeListenFile)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyMetricsPort, 0)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyMetricsListenFile, "file://"+metricsListenFile)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyOPLSyntaxAPIPort, 0)) + require.NoError(t, r.Config(t.Context()).Set(config.KeyOPLSyntaxListenFile, "file://"+oplListenFile)) + + return func(t testing.TB, endpoint string) (string, string, string) { + fp := "" + switch endpoint { + case "read": + fp = readListenFile + case "write": + fp = writeListenFile + case "metrics": + fp = metricsListenFile + case "opl": + fp = oplListenFile + default: + t.Fatalf("unknown endpoint: %q", endpoint) + } + + var addr []byte + var host, port string + + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + addr, err = os.ReadFile(fp) + require.NotEmpty(t, addr) + require.NoError(t, err) + host, port, err = net.SplitHostPort(string(addr)) + require.NoError(t, err) + require.NotEmpty(t, host) + require.NotEmpty(t, port) + }, 2*time.Second, 10*time.Millisecond) + + return host, port, string(addr) + } +} + +func HTTP2TestServer(t testing.TB, handler http.Handler) (*httptest.Server, credentials.TransportCredentials) { + ts := httptest.NewUnstartedServer(handler) + ts.EnableHTTP2 = true + ts.StartTLS() + ts.Client() + t.Cleanup(ts.Close) + + certPool := x509.NewCertPool() + certPool.AddCert(ts.Certificate()) + return ts, credentials.NewTLS(&tls.Config{RootCAs: certPool}) +} diff --git a/internal/e2e/cli_client_test.go b/internal/e2e/cli_client_test.go new file mode 100644 index 0000000..3d5e19b --- /dev/null +++ b/internal/e2e/cli_client_test.go @@ -0,0 +1,155 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1" + + "github.com/ory/herodot" + "github.com/ory/x/cmdx" + + gprclient "github.com/ory/keto/cmd/client" + cliexpand "github.com/ory/keto/cmd/expand" + clirelationtuple "github.com/ory/keto/cmd/relationtuple" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +type cliClient struct { + c *cmdx.CommandExecuter +} + +func (g *cliClient) queryNamespaces(t testing.TB) (res ketoapi.GetNamespacesResponse) { + t.Skip("not implemented for the CLI") + return +} + +var _ client = (*cliClient)(nil) + +func (g *cliClient) oplCheckSyntax(t testing.TB, _ []byte) []*ketoapi.ParseError { + t.Skip("not implemented as a command yet") + return []*ketoapi.ParseError{} +} + +func (g *cliClient) createTuple(t testing.TB, r *ketoapi.RelationTuple) { + tupleEnc, err := json.Marshal(r) + require.NoError(t, err) + + stdout, stderr, err := g.c.Exec(bytes.NewBuffer(tupleEnc), "relation-tuple", "create", "-f", "-") + require.NoError(t, err, "stdout: %s\nstderr: %s", stdout, stderr) + assert.Len(t, stderr, 0, stdout) +} + +func (g *cliClient) assembleQueryFlags(q *ketoapi.RelationQuery, opts []paginationOptionSetter) []string { + var flags []string + if q.Namespace != nil { + flags = append(flags, "--"+clirelationtuple.FlagNamespace, *q.Namespace) + } + if q.SubjectID != nil { + flags = append(flags, "--"+clirelationtuple.FlagSubjectID, *q.SubjectID) + } + if q.SubjectSet != nil { + flags = append(flags, "--"+clirelationtuple.FlagSubjectSet, q.SubjectSet.String()) + } + if q.Relation != nil { + flags = append(flags, "--"+clirelationtuple.FlagRelation, *q.Relation) + } + if q.Object != nil { + flags = append(flags, "--"+clirelationtuple.FlagObject, *q.Object) + } + pagination := getPaginationOptions(opts...) + if pagination.Token != "" { + flags = append(flags, "--"+clirelationtuple.FlagPageToken, pagination.Token) + } + if pagination.Size != 0 { + flags = append(flags, "--"+clirelationtuple.FlagPageSize, strconv.Itoa(pagination.Size)) + } + return flags +} + +func (g *cliClient) queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse { + out := g.c.ExecNoErr(t, append(g.assembleQueryFlags(q, opts), "relation-tuple", "get")...) + + var resp ketoapi.GetResponse + require.NoError(t, json.Unmarshal([]byte(out), &resp), "%s", out) + + return &resp +} + +func (g *cliClient) queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) { + _, stdErr, err := g.c.Exec(nil, append(g.assembleQueryFlags(q, opts), "relation-tuple", "get")...) + assert.Error(t, err) + assert.Contains(t, stdErr, expected.GRPCCodeField.String()) + assert.Contains(t, stdErr, expected.Error()) +} + +func (g *cliClient) check(t testing.TB, r *ketoapi.RelationTuple) bool { + var sub string + if r.SubjectID != nil { + sub = *r.SubjectID + } else { + sub = r.SubjectSet.String() + } + out := g.c.ExecNoErr(t, "check", sub, r.Relation, r.Namespace+":"+r.Object) + var res rts.CheckResponse + require.NoError(t, json.Unmarshal([]byte(out), &res)) + return res.Allowed +} + +func (g *cliClient) batchCheckErr(t testing.TB, _ []*ketoapi.RelationTuple, expected *herodot.DefaultError) { + t.Skip("not implemented for the CLI") +} + +func (g *cliClient) batchCheck(t testing.TB, r []*ketoapi.RelationTuple) []checkResponse { + t.Skip("not implemented for the CLI") + return nil +} + +func (g *cliClient) expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] { + out := g.c.ExecNoErr(t, "expand", r.Relation, r.Namespace+":"+r.Object, "--"+cliexpand.FlagMaxDepth, fmt.Sprintf("%d", depth), "--"+cmdx.FlagFormat, string(cmdx.FormatJSON)) + res := ketoapi.Tree[*ketoapi.RelationTuple]{} + require.NoError(t, json.Unmarshal([]byte(out), &res)) + return &res +} + +func (g *cliClient) waitUntilLive(t testing.TB) { + flags := make([]string, len(g.c.PersistentArgs)) + copy(flags, g.c.PersistentArgs) + + for i, f := range flags { + if f == "--"+cmdx.FlagFormat { + flags = append(flags[:i], flags[i+2:]...) + break + } + } + + ctx, cancel := context.WithTimeout(g.c.Ctx, time.Minute) + defer cancel() + + out := cmdx.ExecNoErrCtx(ctx, t, g.c.New(), append(flags, "status", "--"+gprclient.FlagBlock)...) + require.Equal(t, grpcHealthV1.HealthCheckResponse_SERVING.String()+"\n", out) +} + +func (g *cliClient) deleteTuple(t testing.TB, r *ketoapi.RelationTuple) { + tupleEnc, err := json.Marshal(r) + require.NoError(t, err) + + stdout, stderr, err := g.c.Exec(bytes.NewBuffer(tupleEnc), "relation-tuple", "delete", "-f", "-") + require.NoError(t, err, "stdout: %s\nstderr: %s", stdout, stderr) + assert.Len(t, stderr, 0, stderr) +} + +func (g *cliClient) deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) { + _ = g.c.ExecNoErr(t, append(g.assembleQueryFlags(q, nil), "relation-tuple", "delete-all", "--force")...) +} diff --git a/internal/e2e/full_suit_test.go b/internal/e2e/full_suit_test.go new file mode 100644 index 0000000..2717c14 --- /dev/null +++ b/internal/e2e/full_suit_test.go @@ -0,0 +1,160 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "fmt" + "io" + "net/http" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + "github.com/ory/x/cmdx" + prometheus "github.com/ory/x/prometheusx" + + "github.com/ory/keto/cmd" + cliclient "github.com/ory/keto/cmd/client" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x/dbx" + "github.com/ory/keto/ketoapi" +) + +type ( + transactClient interface { + client + transactTuples(t testing.TB, ins []*ketoapi.RelationTuple, del []*ketoapi.RelationTuple) + } + client interface { + createTuple(t testing.TB, r *ketoapi.RelationTuple) + deleteTuple(t testing.TB, r *ketoapi.RelationTuple) + deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) + queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse + queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) + check(t testing.TB, r *ketoapi.RelationTuple) bool + batchCheck(t testing.TB, r []*ketoapi.RelationTuple) []checkResponse + batchCheckErr(t testing.TB, requestTuples []*ketoapi.RelationTuple, expected *herodot.DefaultError) + expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] + oplCheckSyntax(t testing.TB, content []byte) []*ketoapi.ParseError + waitUntilLive(t testing.TB) + queryNamespaces(t testing.TB) ketoapi.GetNamespacesResponse + } +) + +const ( + promLogLine = "promhttp_metric_handler_requests_total" +) + +func Test(t *testing.T) { + t.Parallel() + for _, dsn := range dbx.GetDSNs(t) { + t.Run(fmt.Sprintf("dsn=%s", dsn.Name), func(t *testing.T) { + t.Parallel() + + ctx, reg, namespaceTestMgr, getAddr := newInitializedReg(t, dsn, nil) + + closeServer := startServer(ctx, t, reg) + t.Cleanup(closeServer) + + _, _, readAddr := getAddr(t, "read") + _, _, writeAddr := getAddr(t, "write") + _, _, oplAddr := getAddr(t, "opl") + _, _, metricsAddr := getAddr(t, "metrics") + + // The test cases start here + // We execute every test with all clients available + for _, cl := range []client{ + newGrpcClient(t, ctx, + readAddr, + writeAddr, + oplAddr, + ), + &restClient{ + readURL: "http://" + readAddr, + writeURL: "http://" + writeAddr, + oplSyntaxURL: "http://" + oplAddr, + }, + &cliClient{c: &cmdx.CommandExecuter{ + New: func() *cobra.Command { + return cmd.NewRootCmd(nil) + }, + Ctx: ctx, + PersistentArgs: []string{ + "--" + cliclient.FlagReadRemote, readAddr, + "--" + cliclient.FlagWriteRemote, writeAddr, + "--insecure-disable-transport-security=true", + "--" + cmdx.FlagFormat, string(cmdx.FormatJSON), + }, + }}, + &sdkClient{ + readRemote: readAddr, + writeRemote: writeAddr, + syntaxRemote: oplAddr, + }, + } { + t.Run(fmt.Sprintf("client=%T", cl), runCases(cl, namespaceTestMgr)) + + if tc, ok := cl.(transactClient); ok { + t.Run(fmt.Sprintf("transactClient=%T", cl), runTransactionCases(tc, namespaceTestMgr)) + } + } + + t.Run("case=metrics are served", func(t *testing.T) { + t.Parallel() + newGrpcClient(t, ctx, + readAddr, + writeAddr, + oplAddr, + ).waitUntilLive(t) + + t.Run("case=on "+prometheus.MetricsPrometheusPath, func(t *testing.T) { + t.Parallel() + resp, err := http.Get(fmt.Sprintf("http://%s%s", metricsAddr, prometheus.MetricsPrometheusPath)) + require.NoError(t, err) + require.Equal(t, resp.StatusCode, http.StatusOK) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.Contains(t, string(body), promLogLine) + }) + + t.Run("case=not on /", func(t *testing.T) { + t.Parallel() + resp, err := http.Get(fmt.Sprintf("http://%s", metricsAddr)) + require.NoError(t, err) + require.Equal(t, resp.StatusCode, http.StatusNotFound) + }) + }) + }) + } +} + +func TestServeCORS(t *testing.T) { + t.Parallel() + + ctx, reg, _, getAddr := newInitializedReg(t, dbx.GetSqlite(t), map[string]any{ + "serve.read.cors.enabled": true, + "serve.read.cors.debug": true, + "serve.read.cors.allowed_methods": []string{http.MethodGet}, + "serve.read.cors.allowed_origins": []string{"https://ory.sh"}, + }) + + closeServer := startServer(ctx, t, reg) + t.Cleanup(closeServer) + + _, _, readAddr := getAddr(t, "read") + waitUntilLive(t, "http://"+readAddr) + t.Log("Health check is ready") + + req, err := http.NewRequest(http.MethodOptions, "http://"+readAddr+relationtuple.ReadRouteBase, nil) + require.NoError(t, err) + req.Header.Set("Origin", "https://ory.sh") + req.Header.Set("Access-Control-Request-Method", "GET") + resp, err := http.DefaultClient.Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, resp.StatusCode) + assert.Equalf(t, "https://ory.sh", resp.Header.Get("Access-Control-Allow-Origin"), "%+v", resp.Header) +} diff --git a/internal/e2e/grpc_client_test.go b/internal/e2e/grpc_client_test.go new file mode 100644 index 0000000..f1504bc --- /dev/null +++ b/internal/e2e/grpc_client_test.go @@ -0,0 +1,285 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "encoding/json" + "math" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + grpcHealthV1 "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + + "github.com/ory/herodot" + + opl "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1" + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +type grpcClient struct { + read, write, oplSyntax *grpc.ClientConn + ctx context.Context +} + +func newGrpcClient(t testing.TB, ctx context.Context, readRemote, writeRemote, oplSyntaxRemote string) *grpcClient { + read, err := grpc.NewClient(readRemote, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, read.Close()) }) + + write, err := grpc.NewClient(writeRemote, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, write.Close()) }) + + oplSyntax, err := grpc.NewClient(oplSyntaxRemote, grpc.WithTransportCredentials(insecure.NewCredentials())) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, oplSyntax.Close()) }) + + ctx, cancel := context.WithCancel(ctx) + t.Cleanup(cancel) + + return &grpcClient{ + read: read, + write: write, + oplSyntax: oplSyntax, + ctx: ctx, + } +} + +func (g *grpcClient) queryNamespaces(t testing.TB) (apiResponse ketoapi.GetNamespacesResponse) { + client := rts.NewNamespacesServiceClient(g.read) + res, err := client.ListNamespaces(g.ctx, &rts.ListNamespacesRequest{}) + require.NoError(t, err) + require.NoError(t, convert(res, &apiResponse)) + + return +} + +var _ transactClient = (*grpcClient)(nil) + +func (g *grpcClient) createTuple(t testing.TB, r *ketoapi.RelationTuple) { + g.transactTuples(t, []*ketoapi.RelationTuple{r}, nil) +} + +func (*grpcClient) createQuery(q *ketoapi.RelationQuery) *rts.RelationQuery { + query := &rts.RelationQuery{ + Namespace: q.Namespace, + Object: q.Object, + Relation: q.Relation, + } + if q.SubjectID != nil { + query.Subject = rts.NewSubjectID(*q.SubjectID) + } else if q.SubjectSet != nil { + query.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation) + } + return query +} + +func requireInt32WithinBounds(t testing.TB, v int) int32 { + require.GreaterOrEqual(t, v, math.MinInt32) + require.LessOrEqual(t, v, math.MaxInt32) + return int32(v) // #nosec G115 -- value validated above +} + +func (g *grpcClient) queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse { + c := rts.NewReadServiceClient(g.read) + pagination := getPaginationOptions(opts...) + + resp, err := c.ListRelationTuples(g.ctx, &rts.ListRelationTuplesRequest{ + RelationQuery: g.createQuery(q), + PageToken: pagination.Token, + PageSize: requireInt32WithinBounds(t, pagination.Size), + }) + require.NoError(t, err) + + irs := make([]*ketoapi.RelationTuple, len(resp.RelationTuples)) + for i := range irs { + irs[i], err = (&ketoapi.RelationTuple{}).FromDataProvider(resp.RelationTuples[i]) + require.NoError(t, err) + } + + return &ketoapi.GetResponse{ + RelationTuples: irs, + NextPageToken: resp.NextPageToken, + } +} + +func (g *grpcClient) queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) { + c := rts.NewReadServiceClient(g.read) + pagination := getPaginationOptions(opts...) + + _, err := c.ListRelationTuples(g.ctx, &rts.ListRelationTuplesRequest{ + RelationQuery: g.createQuery(q), + PageToken: pagination.Token, + PageSize: requireInt32WithinBounds(t, pagination.Size), + }) + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, expected.GRPCCodeField, s.Code(), "%+v", err) +} + +func (g *grpcClient) check(t testing.TB, r *ketoapi.RelationTuple) bool { + c := rts.NewCheckServiceClient(g.read) + + req := &rts.CheckRequest{ + Tuple: &rts.RelationTuple{ + Namespace: r.Namespace, + Object: r.Object, + Relation: r.Relation, + }, + } + if r.SubjectID != nil { + req.Tuple.Subject = rts.NewSubjectID(*r.SubjectID) + } else { + req.Tuple.Subject = rts.NewSubjectSet(r.SubjectSet.Namespace, r.SubjectSet.Object, r.SubjectSet.Relation) + } + resp, err := c.Check(g.ctx, req) + require.NoError(t, err) + + return resp.Allowed +} + +type checkResponse struct { + allowed bool + errorMessage string +} + +func (g *grpcClient) batchCheckErr(t testing.TB, requestTuples []*ketoapi.RelationTuple, expected *herodot.DefaultError) { + _, err := g.doBatchCheck(t, requestTuples) + require.Error(t, err) + s, ok := status.FromError(err) + require.True(t, ok) + assert.Equalf(t, expected.GRPCCodeField, s.Code(), "%+v", err) + assert.Contains(t, s.Message(), expected.Reason()) +} + +func (g *grpcClient) batchCheck(t testing.TB, requestTuples []*ketoapi.RelationTuple) []checkResponse { + resp, err := g.doBatchCheck(t, requestTuples) + require.NoError(t, err) + + checkResponses := make([]checkResponse, len(resp.Results)) + for i, r := range resp.Results { + checkResponses[i] = checkResponse{ + allowed: r.Allowed, + errorMessage: r.Error, + } + } + + return checkResponses +} + +func (g *grpcClient) doBatchCheck(_ testing.TB, requestTuples []*ketoapi.RelationTuple) (*rts.BatchCheckResponse, error) { + c := rts.NewCheckServiceClient(g.read) + + tuples := make([]*rts.RelationTuple, len(requestTuples)) + for i, tuple := range requestTuples { + var subject *rts.Subject + if tuple.SubjectID != nil { + subject = rts.NewSubjectID(*tuple.SubjectID) + } else { + subject = rts.NewSubjectSet(tuple.SubjectSet.Namespace, tuple.SubjectSet.Object, tuple.SubjectSet.Relation) + } + tuples[i] = &rts.RelationTuple{ + Namespace: tuple.Namespace, + Object: tuple.Object, + Relation: tuple.Relation, + Subject: subject, + } + } + + req := &rts.BatchCheckRequest{ + Tuples: tuples, + } + return c.BatchCheck(g.ctx, req) +} + +func (g *grpcClient) expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] { + c := rts.NewExpandServiceClient(g.read) + + resp, err := c.Expand(g.ctx, &rts.ExpandRequest{ + Subject: rts.NewSubjectSet(r.Namespace, r.Object, r.Relation), + MaxDepth: requireInt32WithinBounds(t, depth), + }) + require.NoError(t, err) + + return ketoapi.TreeFromProto[*ketoapi.RelationTuple](resp.Tree) +} + +func (g *grpcClient) waitUntilLive(t testing.TB) { + require.EventuallyWithT(t, func(t *assert.CollectT) { + c := grpcHealthV1.NewHealthClient(g.read) + + res, err := c.Check(g.ctx, &grpcHealthV1.HealthCheckRequest{}) + require.NoError(t, err) + assert.Equal(t, grpcHealthV1.HealthCheckResponse_SERVING, res.Status) + }, 2*time.Second, 10*time.Millisecond) +} + +func (g *grpcClient) deleteTuple(t testing.TB, r *ketoapi.RelationTuple) { + g.transactTuples(t, nil, []*ketoapi.RelationTuple{r}) +} + +func (g *grpcClient) deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) { + c := rts.NewWriteServiceClient(g.write) + query := &rts.RelationQuery{ + Namespace: q.Namespace, + Object: q.Object, + Relation: q.Relation, + } + if q.SubjectID != nil { + query.Subject = rts.NewSubjectID(*q.SubjectID) + } + if q.SubjectSet != nil { + query.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation) + } + _, err := c.DeleteRelationTuples(g.ctx, &rts.DeleteRelationTuplesRequest{ + RelationQuery: query, + }) + require.NoError(t, err) +} + +func (g *grpcClient) transactTuples(t testing.TB, ins []*ketoapi.RelationTuple, del []*ketoapi.RelationTuple) { + c := rts.NewWriteServiceClient(g.write) + + deltas := make([]*rts.RelationTupleDelta, len(ins)+len(del)) + for i := range ins { + deltas[i] = &rts.RelationTupleDelta{ + RelationTuple: ins[i].ToProto(), + Action: rts.RelationTupleDelta_ACTION_INSERT, + } + } + for i := range del { + deltas[len(ins)+i] = &rts.RelationTupleDelta{ + RelationTuple: del[i].ToProto(), + Action: rts.RelationTupleDelta_ACTION_DELETE, + } + } + + _, err := c.TransactRelationTuples(g.ctx, &rts.TransactRelationTuplesRequest{ + RelationTupleDeltas: deltas, + }) + + require.NoError(t, err) +} + +func (g *grpcClient) oplCheckSyntax(t testing.TB, content []byte) (parseErrors []*ketoapi.ParseError) { + c := opl.NewSyntaxServiceClient(g.oplSyntax) + + res, err := c.Check(g.ctx, &opl.CheckRequest{Content: content}) + require.NoError(t, err) + + raw, err := json.Marshal(res.ParseErrors) + require.NoError(t, err) + err = json.Unmarshal(raw, &parseErrors) + require.NoError(t, err) + + return +} diff --git a/internal/e2e/helpers.go b/internal/e2e/helpers.go new file mode 100644 index 0000000..d0da705 --- /dev/null +++ b/internal/e2e/helpers.go @@ -0,0 +1,164 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "encoding/json" + "sync" + "testing" + + "github.com/ory/x/configx" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x/dbx" +) + +type namespaceTestManager struct { + sync.Mutex + reg driver.Registry + ctx context.Context + nspaces []*namespace.Namespace +} + +func (m *namespaceTestManager) add(t testing.TB, nn ...*namespace.Namespace) { + m.Lock() + defer m.Unlock() + + m.nspaces = append(m.nspaces, nn...) + + require.NoError(t, m.reg.Config(m.ctx).Set(config.KeyNamespaces, m.nspaces)) + + t.Cleanup(func() { + for _, n := range nn { + require.NoError(t, m.reg.RelationTupleManager().DeleteAllRelationTuples(m.ctx, &relationtuple.RelationQuery{ + Namespace: &n.Name, + })) + } + }) +} + +func (m *namespaceTestManager) remove(t *testing.T, name string) { + newNamespaces := make([]*namespace.Namespace, 0, len(m.nspaces)) + for _, n := range m.nspaces { + if n.Name != name { + newNamespaces = append(newNamespaces, n) + } + } + m.nspaces = newNamespaces + require.NoError(t, m.reg.Config(m.ctx).Set(config.KeyNamespaces, m.nspaces)) +} + +func newInitializedReg(t testing.TB, dsn *dbx.DsnT, cfgOverwrites map[string]interface{}) (context.Context, driver.Registry, *namespaceTestManager, driver.GetAddr) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancel() + }) + + flags := pflag.NewFlagSet("", pflag.ContinueOnError) + configx.RegisterConfigFlag(flags, nil) + + cfgValues := map[string]interface{}{ + config.KeyDSN: dsn.Conn, + "log.level": "debug", + "log.leak_sensitive_values": true, + config.KeyReadAPIHost: "127.0.0.1", + config.KeyWriteAPIHost: "127.0.0.1", + config.KeyOPLSyntaxAPIHost: "127.0.0.1", + config.KeyMetricsHost: "127.0.0.1", + config.KeyNamespaces: []*namespace.Namespace{}, + config.KeySecretsPagination: []string{"test pagination secret"}, + } + for k, v := range cfgOverwrites { + cfgValues[k] = v + } + + cf := dbx.ConfigFile(t, cfgValues) + require.NoError(t, flags.Parse([]string{"--" + configx.FlagConfig, cf})) + + reg, err := driver.NewDefaultRegistry(ctx, flags, true, nil) + require.NoError(t, err) + + getAddr := driver.UseDynamicPorts(t, reg) + + require.NoError(t, reg.MigrateUp(ctx)) + assertMigrated(ctx, t, reg) + + return ctx, reg, &namespaceTestManager{ + reg: reg, + ctx: ctx, + nspaces: []*namespace.Namespace{}, + }, getAddr +} + +func assertMigrated(ctx context.Context, t testing.TB, r driver.Registry) { + mb, err := r.MigrationBox(ctx) + require.NoError(t, err) + s, err := mb.Status(ctx) + require.NoError(t, err) + assert.False(t, s.HasPending()) +} + +func startServer(ctx context.Context, t testing.TB, reg driver.Registry) func() { + // Start the server + serverCtx, serverCancel := context.WithCancel(ctx) + serverErr := make(chan error) + go func() { + serverErr <- reg.ServeAll(serverCtx) + }() + + // defer this close function to make sure it is shutdown on test failure as well + return func() { + // stop the server + serverCancel() + // wait for it to stop + require.NoError(t, <-serverErr) + } +} + +// convert the struct in `from` to the pointer in `to` using JSON marshal and unmarshal. from and toPtr must have the +// same json field tags. +func convert(from, toPtr any) error { + raw, err := json.Marshal(from) + if err != nil { + return err + } + return json.Unmarshal(raw, toPtr) +} + +type ( + paginationOptions struct { + Token string `json:"page_token"` + Size int `json:"page_size"` + } + paginationOptionSetter func(*paginationOptions) *paginationOptions +) + +func withToken(t string) paginationOptionSetter { + return func(opts *paginationOptions) *paginationOptions { + opts.Token = t + return opts + } +} + +func withSize(size int) paginationOptionSetter { + return func(opts *paginationOptions) *paginationOptions { + opts.Size = size + return opts + } +} + +func getPaginationOptions(modifiers ...paginationOptionSetter) *paginationOptions { + opts := &paginationOptions{} + for _, f := range modifiers { + opts = f(opts) + } + return opts +} diff --git a/internal/e2e/rest_client_test.go b/internal/e2e/rest_client_test.go new file mode 100644 index 0000000..bb74030 --- /dev/null +++ b/internal/e2e/rest_client_test.go @@ -0,0 +1,222 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + + "github.com/ory/herodot" + "github.com/ory/x/healthx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/internal/check" + "github.com/ory/keto/internal/expand" + httpclient "github.com/ory/keto/internal/httpclient" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" + "github.com/ory/keto/schema" +) + +var _ client = &restClient{} + +type restClient struct { + readURL, writeURL, oplSyntaxURL string +} + +func (rc *restClient) queryNamespaces(t testing.TB) (res ketoapi.GetNamespacesResponse) { + body, code := rc.makeRequest(t, http.MethodGet, "/namespaces", "", rc.readURL) + assert.Equal(t, http.StatusOK, code, body) + require.NoError(t, json.Unmarshal([]byte(body), &res)) + + return +} + +func (rc *restClient) oplCheckSyntax(t testing.TB, content []byte) []*ketoapi.ParseError { + body, code := rc.makeRequest(t, http.MethodPost, schema.RouteBase, string(content), rc.oplSyntaxURL) + assert.Equal(t, http.StatusOK, code, body) + var response ketoapi.CheckOPLSyntaxResponse + require.NoError(t, json.Unmarshal([]byte(body), &response)) + + return response.Errors +} + +func (rc *restClient) makeRequest(t testing.TB, method, path, body string, baseURL string) (string, int) { + var b io.Reader + if body != "" { + b = bytes.NewBufferString(body) + } + + // t.Logf("Requesting %s %s%s with body %#v", method, baseURL, path, body) + req, err := http.NewRequest(method, baseURL+path, b) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + respBody, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + + return string(respBody), resp.StatusCode +} + +func (rc *restClient) createTuple(t testing.TB, r *ketoapi.RelationTuple) { + tEnc, err := json.Marshal(r) + require.NoError(t, err) + + body, code := rc.makeRequest(t, http.MethodPut, relationtuple.WriteRouteBase, string(tEnc), rc.writeURL) + assert.Equal(t, http.StatusCreated, code, body) +} + +func (rc *restClient) deleteTuple(t testing.TB, r *ketoapi.RelationTuple) { + body, code := rc.makeRequest(t, http.MethodDelete, relationtuple.WriteRouteBase+"?"+r.ToURLQuery().Encode(), "", rc.writeURL) + require.Equal(t, http.StatusNoContent, code, body) +} + +func (rc *restClient) deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) { + body, code := rc.makeRequest(t, http.MethodDelete, relationtuple.WriteRouteBase+"?"+q.ToURLQuery().Encode(), "", rc.writeURL) + require.Equal(t, http.StatusNoContent, code, body) +} + +func (rc *restClient) queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse { + urlQuery := q.ToURLQuery() + + pagination := getPaginationOptions(opts...) + if pagination.Size != 0 { + urlQuery.Set("page_size", strconv.Itoa(pagination.Size)) + } + if pagination.Token != "" { + urlQuery.Set("page_token", pagination.Token) + } + + body, code := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", relationtuple.ReadRouteBase, urlQuery.Encode()), "", rc.readURL) + require.Equal(t, http.StatusOK, code, body) + + var dec ketoapi.GetResponse + require.NoError(t, json.Unmarshal([]byte(body), &dec)) + + return &dec +} + +func (rc *restClient) queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) { + urlQuery := q.ToURLQuery() + + pagination := getPaginationOptions(opts...) + if pagination.Size != 0 { + urlQuery.Set("page_size", strconv.Itoa(pagination.Size)) + } + if pagination.Token != "" { + urlQuery.Set("page_token", pagination.Token) + } + + body, code := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", relationtuple.ReadRouteBase, urlQuery.Encode()), "", rc.readURL) + + assert.Equal(t, expected.CodeField, code) + assert.Equal(t, int64(expected.StatusCode()), gjson.Get(body, "error.code").Int()) + assert.Equal(t, expected.Status(), gjson.Get(body, "error.status").String()) + assert.Equal(t, expected.Error(), gjson.Get(body, "error.message").String(), body) +} + +func (rc *restClient) check(t testing.TB, r *ketoapi.RelationTuple) bool { + q := r.ToURLQuery() + bodyGet, codeGet := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", check.RouteBase, q.Encode()), "", rc.readURL) + + var respGet rts.CheckResponse + require.NoError(t, json.Unmarshal([]byte(bodyGet), &respGet)) + + j, err := json.Marshal(r) + require.NoError(t, err) + bodyPost, codePost := rc.makeRequest(t, http.MethodPost, check.RouteBase, string(j), rc.readURL) + + var respPost rts.CheckResponse + require.NoError(t, json.Unmarshal([]byte(bodyPost), &respPost)) + + if codeGet == http.StatusOK && codePost == http.StatusOK { + assert.Equal(t, true, respGet.Allowed, "%s", bodyGet) + assert.Equal(t, true, respPost.Allowed, "%s", bodyPost) + return true + } + + assert.Equal(t, http.StatusForbidden, codeGet, bodyGet) + assert.Equal(t, http.StatusForbidden, codePost, bodyPost) + assert.Equal(t, false, respGet.Allowed) + assert.Equal(t, false, respPost.Allowed) + return false +} + +func (rc *restClient) batchCheckErr(t testing.TB, requestTuples []*ketoapi.RelationTuple, expected *herodot.DefaultError) { + req := httpclient.BatchCheckPermissionBody{ + Tuples: tuplesToRelationships(requestTuples), + } + j, err := json.Marshal(req) + require.NoError(t, err) + body, code := rc.makeRequest(t, http.MethodPost, check.BatchRoute, string(j), rc.readURL) + assert.Equal(t, expected.CodeField, code) + assert.Contains(t, body, expected.Reason()) +} + +func (rc *restClient) batchCheck(t testing.TB, requestTuples []*ketoapi.RelationTuple) []checkResponse { + req := httpclient.BatchCheckPermissionBody{ + Tuples: tuplesToRelationships(requestTuples), + } + j, err := json.Marshal(req) + require.NoError(t, err) + body, code := rc.makeRequest(t, http.MethodPost, check.BatchRoute, string(j), rc.readURL) + require.Equal(t, http.StatusOK, code, "batch check failed unexpected with error code %d", code) + + var respPost rts.BatchCheckResponse + require.NoError(t, json.Unmarshal([]byte(body), &respPost)) + + responseChecks := make([]checkResponse, len(respPost.Results)) + for i, result := range respPost.Results { + responseChecks[i] = checkResponse{ + allowed: result.Allowed, + errorMessage: result.Error, + } + } + return responseChecks +} + +func (rc *restClient) expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] { + query := r.ToURLQuery() + query.Set("max-depth", fmt.Sprintf("%d", depth)) + + body, code := rc.makeRequest(t, http.MethodGet, fmt.Sprintf("%s?%s", expand.RouteBase, query.Encode()), "", rc.readURL) + require.Equal(t, http.StatusOK, code, body) + + tree := &ketoapi.Tree[*ketoapi.RelationTuple]{} + require.NoError(t, json.Unmarshal([]byte(body), tree)) + + return tree +} + +func healthReady(t require.TestingT, readURL string) int { + req, err := http.NewRequest("GET", readURL+healthx.ReadyCheckPath, nil) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) //nolint:gosec // Test code with controlled URL. + if err != nil { + return 0 + } + defer func() { _ = resp.Body.Close() }() + return resp.StatusCode +} + +func waitUntilLive(t testing.TB, readURL string) { + require.EventuallyWithT(t, func(t *assert.CollectT) { + assert.Equal(t, http.StatusOK, healthReady(t, readURL)) + }, 5*time.Second, 10*time.Millisecond) +} + +func (rc *restClient) waitUntilLive(t testing.TB) { + waitUntilLive(t, rc.readURL) +} diff --git a/internal/e2e/sdk_client_test.go b/internal/e2e/sdk_client_test.go new file mode 100644 index 0000000..356b20d --- /dev/null +++ b/internal/e2e/sdk_client_test.go @@ -0,0 +1,360 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + + httpclient "github.com/ory/keto/internal/httpclient" + "github.com/ory/keto/ketoapi" +) + +type ( + sdkClient struct { + rc, wc, sc *httpclient.APIClient + readRemote, writeRemote, syntaxRemote string + } +) + +var _ client = (*sdkClient)(nil) + +var requestTimeout = 5 * time.Second + +func (c *sdkClient) requestCtx(t testing.TB) context.Context { + ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) + t.Cleanup(cancel) + return ctx +} + +func (c *sdkClient) oplCheckSyntax(t testing.TB, content []byte) (parseErrors []*ketoapi.ParseError) { + res, _, err := c.getOPLSyntaxClient(). + RelationshipApi. + CheckOplSyntax(c.requestCtx(t)). + Body(string(content)). + Execute() + require.NoError(t, err) + + raw, err := json.Marshal(res.Errors) + require.NoError(t, err) + err = json.Unmarshal(raw, &parseErrors) + require.NoError(t, err) + + return +} + +func (c *sdkClient) getReadClient() *httpclient.APIClient { + if c.rc == nil { + cfg := httpclient.NewConfiguration() + cfg.Host = c.readRemote + cfg.Scheme = "http" + c.rc = httpclient.NewAPIClient(cfg) + } + return c.rc +} + +func (c *sdkClient) getWriteClient() *httpclient.APIClient { + if c.wc == nil { + cfg := httpclient.NewConfiguration() + cfg.Host = c.writeRemote + cfg.Scheme = "http" + c.wc = httpclient.NewAPIClient(cfg) + } + return c.wc +} + +func (c *sdkClient) getOPLSyntaxClient() *httpclient.APIClient { + if c.sc == nil { + cfg := httpclient.NewConfiguration() + cfg.Host = c.syntaxRemote + cfg.Scheme = "http" + c.sc = httpclient.NewAPIClient(cfg) + } + return c.sc +} + +func (c *sdkClient) createTuple(t testing.TB, r *ketoapi.RelationTuple) { + payload := httpclient.CreateRelationshipBody{ + Namespace: new(r.Namespace), + Object: new(r.Object), + Relation: new(r.Relation), + SubjectId: r.SubjectID, + } + if r.SubjectID == nil { + payload.SubjectSet = &httpclient.SubjectSet{ + Namespace: r.SubjectSet.Namespace, + Object: r.SubjectSet.Object, + Relation: r.SubjectSet.Relation, + } + } + + _, _, err := c.getWriteClient().RelationshipApi. + CreateRelationship(c.requestCtx(t)). + CreateRelationshipBody(payload). + Execute() + require.NoError(t, err) +} + +func withSubject[P interface { + SubjectId(string) P + SubjectSetNamespace(string) P + SubjectSetObject(string) P + SubjectSetRelation(string) P +}](params P, subID *string, subSet *ketoapi.SubjectSet) P { + if subID != nil { + return params.SubjectId(*subID) + } + if subSet != nil { + return params. + SubjectSetNamespace(subSet.Namespace). + SubjectSetObject(subSet.Object). + SubjectSetRelation(subSet.Relation) + } + return params +} + +func (c *sdkClient) deleteTuple(t testing.TB, r *ketoapi.RelationTuple) { + request := c.getWriteClient().RelationshipApi. + DeleteRelationships(c.requestCtx(t)). + Namespace(r.Namespace). + Object(r.Object). + Relation(r.Relation) + request = withSubject(request, r.SubjectID, r.SubjectSet) + + _, err := request.Execute() + require.NoError(t, err) +} + +func (c *sdkClient) deleteAllTuples(t testing.TB, q *ketoapi.RelationQuery) { + request := c.getWriteClient().RelationshipApi.DeleteRelationships(c.requestCtx(t)) + if q.Namespace != nil { + request = request.Namespace(*q.Namespace) + } + if q.Object != nil { + request = request.Object(*q.Object) + } + if q.Relation != nil { + request = request.Relation(*q.Relation) + } + request = withSubject(request, q.SubjectID, q.SubjectSet) + _, err := request.Execute() + require.NoError(t, err) +} + +func compileParams(req httpclient.RelationshipApiApiGetRelationshipsRequest, q *ketoapi.RelationQuery, opts []paginationOptionSetter) httpclient.RelationshipApiApiGetRelationshipsRequest { + if q.Namespace != nil { + req = req.Namespace(*q.Namespace) + } + if q.Relation != nil { + req = req.Relation(*q.Relation) + } + if q.Object != nil { + req = req.Object(*q.Object) + } + if q.SubjectID != nil { + req = req.SubjectId(*q.SubjectID) + } + if q.SubjectSet != nil { + req = req. + SubjectSetNamespace(q.SubjectSet.Namespace). + SubjectSetObject(q.SubjectSet.Object). + SubjectSetRelation(q.SubjectSet.Relation) + } + + pagination := getPaginationOptions(opts...) + if pagination.Size != 0 { + req = req.PageSize(int64(pagination.Size)) + } + if pagination.Token != "" { + req = req.PageToken(pagination.Token) + } + + return req +} + +func (c *sdkClient) queryTuple(t testing.TB, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) *ketoapi.GetResponse { + request := c.getReadClient().RelationshipApi.GetRelationships(c.requestCtx(t)) + request = compileParams(request, q, opts) + + resp, _, err := request.Execute() + require.NoError(t, err) + + getResp := &ketoapi.GetResponse{ + RelationTuples: make([]*ketoapi.RelationTuple, len(resp.RelationTuples)), + NextPageToken: resp.GetNextPageToken(), + } + + for i, rt := range resp.RelationTuples { + getResp.RelationTuples[i] = &ketoapi.RelationTuple{ + Namespace: rt.Namespace, + Object: rt.Object, + Relation: rt.Relation, + } + if rt.SubjectSet != nil { + getResp.RelationTuples[i].SubjectSet = &ketoapi.SubjectSet{ + Namespace: rt.SubjectSet.Namespace, + Object: rt.SubjectSet.Object, + Relation: rt.SubjectSet.Relation, + } + } else { + getResp.RelationTuples[i].SubjectID = rt.SubjectId + } + } + + return getResp +} + +func (c *sdkClient) queryTupleErr(t testing.TB, expected *herodot.DefaultError, q *ketoapi.RelationQuery, opts ...paginationOptionSetter) { + request := c.getReadClient().RelationshipApi.GetRelationships(c.requestCtx(t)) + request = compileParams(request, q, opts) + _, _, err := request.Execute() + + switch err.(type) { + case nil: + require.FailNow(t, "expected error but got nil") + case *httpclient.GenericOpenAPIError: + assert.Equal(t, expected.CodeField, http.StatusNotFound) + default: + require.FailNow(t, "got unknown error %+v\nexpected %+v", err, expected) + } +} + +func (c *sdkClient) check(t testing.TB, r *ketoapi.RelationTuple) bool { + request := c.getReadClient().PermissionApi.CheckPermission(c.requestCtx(t)). + Namespace(r.Namespace). + Object(r.Object). + Relation(r.Relation) + request = withSubject(request, r.SubjectID, r.SubjectSet) + + resp, _, err := request.Execute() + require.NoError(t, err) + + return resp.GetAllowed() +} + +func (c *sdkClient) batchCheckErr(t testing.TB, requestTuples []*ketoapi.RelationTuple, expected *herodot.DefaultError) { + request := c.getReadClient().PermissionApi.BatchCheckPermission(c.requestCtx(t)). + BatchCheckPermissionBody(httpclient.BatchCheckPermissionBody{ + Tuples: tuplesToRelationships(requestTuples), + }) + + _, _, err := request.Execute() + switch typedErr := err.(type) { + case nil: + require.FailNow(t, "expected error but got nil") + case *httpclient.GenericOpenAPIError: + assert.Contains(t, typedErr.Error(), expected.Reason()) + default: + require.FailNow(t, "got unknown error %+v\nexpected %+v", err, expected) + } +} + +func (c *sdkClient) batchCheck(t testing.TB, requestTuples []*ketoapi.RelationTuple) []checkResponse { + request := c.getReadClient().PermissionApi.BatchCheckPermission(c.requestCtx(t)). + BatchCheckPermissionBody(httpclient.BatchCheckPermissionBody{ + Tuples: tuplesToRelationships(requestTuples), + }) + + resp, _, err := request.Execute() + require.NoError(t, err) + + responses := make([]checkResponse, len(resp.Results)) + for i, result := range resp.Results { + errMsg := "" + if result.Error != nil { + errMsg = *result.Error + } + responses[i] = checkResponse{ + allowed: result.Allowed, + errorMessage: errMsg, + } + } + return responses +} + +func tuplesToRelationships(tuples []*ketoapi.RelationTuple) []httpclient.Relationship { + relationships := make([]httpclient.Relationship, len(tuples)) + for i, requestTuple := range tuples { + relationship := httpclient.Relationship{ + Namespace: requestTuple.Namespace, + Object: requestTuple.Object, + Relation: requestTuple.Relation, + SubjectId: requestTuple.SubjectID, + } + if requestTuple.SubjectSet != nil { + relationship.SubjectSet = &httpclient.SubjectSet{ + Namespace: requestTuple.SubjectSet.Namespace, + Object: requestTuple.SubjectSet.Object, + Relation: requestTuple.SubjectSet.Relation, + } + } + relationships[i] = relationship + } + return relationships +} + +func buildTree(t testing.TB, mt *httpclient.ExpandedPermissionTree) *ketoapi.Tree[*ketoapi.RelationTuple] { + result := &ketoapi.Tree[*ketoapi.RelationTuple]{ + Type: ketoapi.TreeNodeType(mt.Type), + } + if mt.Tuple.SubjectSet != nil { + result.Tuple = &ketoapi.RelationTuple{ + SubjectSet: &ketoapi.SubjectSet{ + Namespace: mt.Tuple.SubjectSet.Namespace, + Object: mt.Tuple.SubjectSet.Object, + Relation: mt.Tuple.SubjectSet.Relation, + }, + } + } else { + result.Tuple = &ketoapi.RelationTuple{ + SubjectID: mt.Tuple.SubjectId, + } + } + + if result.Type != ketoapi.TreeNodeLeaf && len(mt.Children) != 0 { + result.Children = make([]*ketoapi.Tree[*ketoapi.RelationTuple], len(mt.Children)) + for i, c := range mt.Children { + c := c + result.Children[i] = buildTree(t, &c) + } + } + return result +} + +func (c *sdkClient) expand(t testing.TB, r *ketoapi.SubjectSet, depth int) *ketoapi.Tree[*ketoapi.RelationTuple] { + request := c.getReadClient().PermissionApi.ExpandPermissions(c.requestCtx(t)). + Namespace(r.Namespace). + Object(r.Object). + Relation(r.Relation). + MaxDepth(int64(depth)) + + resp, _, err := request.Execute() + require.NoError(t, err) + + return buildTree(t, resp) +} + +func (c *sdkClient) waitUntilLive(t testing.TB) { + resp, _, err := c.getReadClient().MetadataApi.IsReady(c.requestCtx(t)).Execute() + for err != nil { + resp, _, err = c.getReadClient().MetadataApi.IsReady(c.requestCtx(t)).Execute() + } + require.Equal(t, "ok", resp.Status) +} + +func (c *sdkClient) queryNamespaces(t testing.TB) (response ketoapi.GetNamespacesResponse) { + res, _, err := c.getReadClient().RelationshipApi.ListRelationshipNamespaces(c.requestCtx(t)).Execute() + require.NoError(t, err) + require.NoError(t, convert(res, &response)) + + return +} diff --git a/internal/e2e/testcases_test.go b/internal/e2e/testcases_test.go new file mode 100644 index 0000000..2424ddc --- /dev/null +++ b/internal/e2e/testcases_test.go @@ -0,0 +1,401 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "fmt" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/ketoapi" +) + +func runCases(c client, m *namespaceTestManager) func(*testing.T) { + return func(t *testing.T) { + c.waitUntilLive(t) + + t.Run("case=list namespaces", func(t *testing.T) { + first := namespace.Namespace{Name: "my namespace"} + second := namespace.Namespace{Name: "my other namespace"} + m.add(t, &first, &second) + + resp := c.queryNamespaces(t) + assert.GreaterOrEqual(t, len(resp.Namespaces), 2) + assertNamespacesContains(t, resp.Namespaces, "my namespace") + assertNamespacesContains(t, resp.Namespaces, "my other namespace") + }) + + t.Run("case=gets empty namespace", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name}) + assert.Len(t, resp.RelationTuples, 0) + }) + + t.Run("case=creates tuple and uses it then", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + tuple := &ketoapi.RelationTuple{ + Namespace: n.Name, + Object: fmt.Sprintf("object for client %T", c), + Relation: "access", + SubjectID: new("client"), + } + + c.createTuple(t, tuple) + + resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &tuple.Namespace}) + require.Len(t, resp.RelationTuples, 1) + assert.Equal(t, tuple, resp.RelationTuples[0]) + + // try the check API to see whether the tuple is interpreted correctly + assert.True(t, c.check(t, tuple)) + batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tuple}) + require.Len(t, batchResult, 1) + assert.True(t, batchResult[0].allowed) + assert.Empty(t, batchResult[0].errorMessage) + }) + + t.Run("case=creates tuple with empty IDs", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + tuples := []*ketoapi.RelationTuple{{ + Namespace: n.Name, + Object: "", + Relation: "access", + SubjectID: new(""), + }, { + Namespace: n.Name, + Object: "", + Relation: "access", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: "", + Relation: "access", + }, + }} + + for _, tp := range tuples { + c.createTuple(t, tp) + // try the check API to see whether the tuple is interpreted correctly + assert.True(t, c.check(t, tp)) + batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{tp}) + require.Len(t, batchResult, 1) + assert.True(t, batchResult[0].allowed) + assert.Empty(t, batchResult[0].errorMessage) + } + + resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name}) + assert.ElementsMatch(t, tuples, resp.RelationTuples) + }) + + t.Run("case=check subjectSet relations", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + obj := fmt.Sprintf("obj for client %T", c) + rel := "check" + + rt := &ketoapi.RelationTuple{ + Namespace: n.Name, + Object: obj, + Relation: rel, + SubjectSet: &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: obj, + Relation: rel, + }, + } + c.createTuple(t, rt) + + assert.True(t, c.check(t, rt)) + batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{rt}) + require.Len(t, batchResult, 1) + assert.True(t, batchResult[0].allowed) + assert.Empty(t, batchResult[0].errorMessage) + }) + + t.Run("case=batch check", func(t *testing.T) { + /* + Test batch check with four cases: + - Allowed single subject tuple + - Allowed subject set tuple + - Tuple with unknown namespace (will return an error for this case) + - Dis-allowed tuple + */ + namespace1 := &namespace.Namespace{Name: t.Name()} + namespace2 := &namespace.Namespace{Name: t.Name() + "-2"} + m.add(t, namespace1) + m.add(t, namespace2) + + obj1 := fmt.Sprintf("obj for client %T", c) + obj2 := fmt.Sprintf("another obj for client %T", c) + rel1 := "check" + rel2 := "access" + + tupleSubjectSet := &ketoapi.RelationTuple{ + Namespace: namespace1.Name, + Object: obj1, + Relation: rel1, + SubjectSet: &ketoapi.SubjectSet{ + Namespace: namespace1.Name, + Object: obj1, + Relation: rel1, + }, + } + c.createTuple(t, tupleSubjectSet) + + tupleSingleSubject := &ketoapi.RelationTuple{ + Namespace: namespace2.Name, + Object: obj2, + Relation: rel2, + SubjectID: new("sub1"), + } + c.createTuple(t, tupleSingleSubject) + + unknownNamespaceTuple := &ketoapi.RelationTuple{ + Namespace: "unknown-namespace", + Object: obj1, + Relation: rel1, + SubjectID: new("sub1"), + } + + unknownSubjectTuple := &ketoapi.RelationTuple{ + Namespace: namespace1.Name, + Object: obj1, + Relation: rel1, + SubjectID: new("unknown-sub"), + } + + batchResult := c.batchCheck(t, []*ketoapi.RelationTuple{ + tupleSubjectSet, tupleSingleSubject, + unknownNamespaceTuple, unknownSubjectTuple, + }) + require.Len(t, batchResult, 4) + assert.True(t, batchResult[0].allowed) + assert.Empty(t, batchResult[0].errorMessage) + assert.True(t, batchResult[1].allowed) + assert.Empty(t, batchResult[1].errorMessage) + assert.False(t, batchResult[2].allowed) + assert.Contains(t, batchResult[2].errorMessage, "The requested resource could not be found") + assert.False(t, batchResult[3].allowed) + assert.Empty(t, batchResult[3].errorMessage) + + // Verify a call with no tuples returns successfully with no results + emptyResults := c.batchCheck(t, []*ketoapi.RelationTuple{}) + require.Empty(t, emptyResults) + }) + + t.Run("case=batch check validation errors", func(t *testing.T) { + // Pass in 11 tuples to check, more than the default limit of 10, and verify the request is rejected + tuples := make([]*ketoapi.RelationTuple, 11) + for i := range tuples { + tuples[i] = &ketoapi.RelationTuple{ + Namespace: "namespace-name", + Object: "obj", + Relation: "rel", + SubjectID: new("sub"), + } + } + c.batchCheckErr(t, tuples, herodot.ErrBadRequest()) + }) + + t.Run("case=expand API", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + obj := fmt.Sprintf("tree for client %T", c) + rel := "expand" + + subjects := []string{"s1", "s2"} + expectedTree := &ketoapi.Tree[*ketoapi.RelationTuple]{ + Type: ketoapi.TreeNodeUnion, + Tuple: &ketoapi.RelationTuple{ + SubjectSet: &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: obj, + Relation: rel, + }, + }, + Children: make([]*ketoapi.Tree[*ketoapi.RelationTuple], len(subjects)), + } + + for i, subjectID := range subjects { + c.createTuple(t, &ketoapi.RelationTuple{ + Namespace: n.Name, + Object: obj, + Relation: rel, + SubjectID: &subjectID, + }) + expectedTree.Children[i] = &ketoapi.Tree[*ketoapi.RelationTuple]{ + Type: ketoapi.TreeNodeLeaf, + Tuple: &ketoapi.RelationTuple{ + SubjectID: &subjectID, + }, + } + } + + actualTree := c.expand(t, expectedTree.Tuple.SubjectSet, 100) + + assert.Equal(t, expectedTree.Type, actualTree.Type) + assert.Equal(t, expectedTree.Tuple, actualTree.Tuple) + assert.Len(t, actualTree.Children, len(expectedTree.Children)) + expand.AssertExternalTreesAreEqual(t, expectedTree, actualTree) + }) + + t.Run("case=gets result paginated", func(t *testing.T) { + const nTuples = 10 + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + rel := fmt.Sprintf("some unique relation %T", c) + for i := 0; i < nTuples; i++ { + c.createTuple(t, &ketoapi.RelationTuple{ + Namespace: n.Name, + Object: "o" + strconv.Itoa(i), + Relation: rel, + SubjectID: new("s" + strconv.Itoa(i)), + }) + } + + var ( + resp ketoapi.GetResponse + nPages int + ) + // do ... while resp.NextPageToken != "" + for ok := true; ok; ok = resp.NextPageToken != "" { + resp = *c.queryTuple(t, + &ketoapi.RelationQuery{ + Namespace: &n.Name, + Relation: &rel, + }, + withToken(resp.NextPageToken), + withSize(1), + ) + nPages++ + assert.Len(t, resp.RelationTuples, 1) + } + + assert.Equal(t, nTuples, nPages) + }) + + t.Run("case=deletes tuple", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + for _, rt := range []*ketoapi.RelationTuple{ + { + SubjectID: new("s"), + }, + { + SubjectSet: &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: "so", + Relation: "rel", + }, + }, + } { + t.Run(fmt.Sprintf("subject_id=%v", rt.SubjectID == nil), func(t *testing.T) { + rt.Namespace = n.Name + rt.Object = "o" + rt.Relation = "rel" + c.createTuple(t, rt) + + resp := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name}) + assert.Equal(t, []*ketoapi.RelationTuple{rt}, resp.RelationTuples) + + c.deleteTuple(t, rt) + + resp = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: &n.Name}) + assert.Len(t, resp.RelationTuples, 0) + }) + } + }) + + t.Run("case=deletes tuples based on relation query", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + rts := []*ketoapi.RelationTuple{ + { + Namespace: n.Name, + Object: "o1", + Relation: "rel", + SubjectID: new("s1"), + }, + { + Namespace: n.Name, + Object: "o2", + Relation: "rel", + SubjectID: new("s2"), + }, + } + for i := 0; i < len(rts); i++ { + c.createTuple(t, rts[i]) + } + + q := &ketoapi.RelationQuery{ + Namespace: &n.Name, + Relation: new("rel"), + } + resp := c.queryTuple(t, q) + require.ElementsMatch(t, resp.RelationTuples, rts) + + c.deleteAllTuples(t, q) + resp = c.queryTuple(t, q) + assert.Equal(t, 0, len(resp.RelationTuples)) + }) + + t.Run("case=returns error with status code on unknown namespace", func(t *testing.T) { + c.queryTupleErr(t, herodot.ErrNotFound(), &ketoapi.RelationQuery{Namespace: new("unknown namespace")}) + }) + + t.Run("case=still serves tuples from deleted namespace", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + tuple := &ketoapi.RelationTuple{ + Namespace: n.Name, + Object: "o", + Relation: "rel", + SubjectID: new("s"), + } + c.createTuple(t, tuple) + + m.remove(t, n.Name) + + resp := c.queryTuple(t, &ketoapi.RelationQuery{}) + assert.Equal(t, []*ketoapi.RelationTuple{tuple}, resp.RelationTuples) + + // Add the namespace again here, so that we can clean up properly. + m.add(t, n) + }) + + t.Run("case=OPL syntax check", func(t *testing.T) { + parseErrors := c.oplCheckSyntax(t, []byte("/* unclosed comment")) + require.Len(t, parseErrors, 1) + assert.Contains(t, parseErrors[0].Message, "unclosed comment") + }) + } +} + +func assertNamespacesContains(t *testing.T, list []ketoapi.Namespace, name string) { + t.Helper() + for _, n := range list { + if n.Name == name { + return + } + } + t.Errorf("Could not find %q in %+v", name, list) +} diff --git a/internal/e2e/transaction_cases_test.go b/internal/e2e/transaction_cases_test.go new file mode 100644 index 0000000..73cc470 --- /dev/null +++ b/internal/e2e/transaction_cases_test.go @@ -0,0 +1,232 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "cmp" + "slices" + "strconv" + "testing" + "time" + + "github.com/ory/x/uuidx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/ketoapi" +) + +func runTransactionCases(c transactClient, m *namespaceTestManager) func(*testing.T) { + return func(t *testing.T) { + t.Parallel() + t.Run("case=create and delete", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + + tuples := []*ketoapi.RelationTuple{ + { + Namespace: n.Name, + Object: "o", + Relation: "rel", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: "o", + Relation: "rel", + }, + }, + { + Namespace: n.Name, + Object: "o", + Relation: "rel", + SubjectID: new("sid"), + }, + } + + c.transactTuples(t, tuples, nil) + + resp := c.queryTuple(t, &ketoapi.RelationQuery{ + Namespace: &n.Name, + }) + for i := range tuples { + assert.Contains(t, resp.RelationTuples, tuples[i]) + } + + c.transactTuples(t, nil, tuples) + + resp = c.queryTuple(t, &ketoapi.RelationQuery{ + Namespace: &n.Name, + }) + assert.Len(t, resp.RelationTuples, 0) + }) + + t.Run("case=duplicate string representations", func(t *testing.T) { + n := &namespace.Namespace{Name: t.Name()} + m.add(t, n) + c.transactTuples(t, []*ketoapi.RelationTuple{ + { + Namespace: n.Name, + Object: "o", + Relation: "rel", + SubjectID: new("sid"), + }, + { + Namespace: n.Name, + Object: "o", + Relation: "rel", + SubjectID: new("sid"), + }, + }, nil) + }) + + t.Run("case=large inserts and deletes", func(t *testing.T) { + if testing.Short() { + t.Skip("This test is fairly expensive, especially the deletion.") + } + + ns := []*namespace.Namespace{ + {Name: uuidx.NewV4().String()}, + {Name: uuidx.NewV4().String()}, + } + m.add(t, ns...) + + var tuples []*ketoapi.RelationTuple + for i := range 1200 { + tuples = append(tuples, + &ketoapi.RelationTuple{ + Namespace: ns[0].Name, + Object: "o" + strconv.Itoa(i), + Relation: "rela", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: ns[1].Name, + Object: "o" + strconv.Itoa(i), + Relation: "relx", + }, + }, + &ketoapi.RelationTuple{ + Namespace: ns[0].Name, + Object: "o" + strconv.Itoa(i), + Relation: "relb", + SubjectID: new("sid"), + }, + ) + } + + t0 := time.Now() + c.transactTuples(t, tuples, nil) + t.Logf("insert took %s", time.Since(t0)) + + t0 = time.Now() + var resp []*ketoapi.RelationTuple + var pt string + for { + r := c.queryTuple(t, &ketoapi.RelationQuery{ + Namespace: &ns[0].Name, + }, withSize(1000), withToken(pt)) + resp = append(resp, r.RelationTuples...) + pt = r.NextPageToken + if pt == "" { + break + } + } + t.Logf("query took %s", time.Since(t0)) + + sort := func(a, b *ketoapi.RelationTuple) int { + return cmp.Or( + cmp.Compare(a.Namespace, b.Namespace), + cmp.Compare(a.Object, b.Object), + cmp.Compare(a.Relation, b.Relation), + ) + } + slices.SortFunc(resp, sort) + slices.SortFunc(tuples, sort) + + require.Equal(t, tuples, resp) + + t0 = time.Now() + c.transactTuples(t, nil, tuples) + t.Logf("delete took %s", time.Since(t0)) + + resp = c.queryTuple(t, &ketoapi.RelationQuery{ + Namespace: &ns[0].Name, + }).RelationTuples + assert.Len(t, resp, 0) + }) + + t.Run("case=expand-api-display-access docs code sample", func(t *testing.T) { + files := &namespace.Namespace{Name: t.Name() + "files"} + directories := &namespace.Namespace{Name: t.Name() + "directories"} + m.add(t, files, directories) + + tuples := []*ketoapi.RelationTuple{ + { + Namespace: directories.Name, + Object: "/photos", + Relation: "owner", + SubjectID: new("maureen"), + }, + { + Namespace: files.Name, + Object: "/photos/beach.jpg", + Relation: "owner", + SubjectID: new("maureen"), + }, + { + Namespace: files.Name, + Object: "/photos/mountains.jpg", + Relation: "owner", + SubjectID: new("laura"), + }, + { + Namespace: directories.Name, + Object: "/photos", + Relation: "access", + SubjectID: new("laura"), + }, + } + for _, o := range []struct{ n, o string }{ + {files.Name, "/photos/beach.jpg"}, + {files.Name, "/photos/mountains.jpg"}, + {directories.Name, "/photos"}, + } { + tuples = append(tuples, &ketoapi.RelationTuple{ + Namespace: o.n, + Object: o.o, + Relation: "access", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: o.n, + Object: o.o, + Relation: "owner", + }, + }) + } + for _, obj := range []string{"/photos/beach.jpg", "/photos/mountains.jpg"} { + tuples = append(tuples, &ketoapi.RelationTuple{ + Namespace: files.Name, + Object: obj, + Relation: "access", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: directories.Name, + Object: "/photos", + Relation: "access", + }, + }) + } + + c.transactTuples(t, tuples, nil) + + fileTuples := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(files.Name)}) + directoryTuples := c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(directories.Name)}) + allTuples := append(fileTuples.RelationTuples, directoryTuples.RelationTuples...) + assert.ElementsMatch(t, allTuples, tuples) + + c.transactTuples(t, nil, tuples) + + fileTuples = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(files.Name)}) + directoryTuples = c.queryTuple(t, &ketoapi.RelationQuery{Namespace: new(directories.Name)}) + allTuples = append(fileTuples.RelationTuples, directoryTuples.RelationTuples...) + assert.Len(t, allTuples, 0) + }) + } +} diff --git a/internal/expand/engine.go b/internal/expand/engine.go new file mode 100644 index 0000000..8bb0178 --- /dev/null +++ b/internal/expand/engine.go @@ -0,0 +1,397 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand + +import ( + "context" + "slices" + + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" + "github.com/ory/keto/x/events" +) + +type ( + EngineDependencies interface { + relationtuple.ManagerProvider + config.Provider + logrusx.Provider + otelx.Provider + } + Engine struct { + d EngineDependencies + } + EngineProvider interface { + ExpandEngine() *Engine + } +) + +func NewEngine(d EngineDependencies) *Engine { + return &Engine{ + d: d, + } +} + +// treeBuilder holds state shared across a single BuildTree call. +type treeBuilder struct { + d EngineDependencies + namespaceManager namespace.Manager + nodeCount int + maxNodes int + visited map[relationtuple.SubjectSet]struct{} + isStrict bool + expandRewrites bool +} + +func (e *Engine) newTreeBuilder(ctx context.Context, namespaceManager namespace.Manager) *treeBuilder { + return &treeBuilder{ + d: e.d, + namespaceManager: namespaceManager, + visited: make(map[relationtuple.SubjectSet]struct{}), + maxNodes: e.d.Config(ctx).ExpandMaxTupleCount(), + nodeCount: 0, + isStrict: e.d.Config(ctx).StrictMode(), + expandRewrites: e.d.Config(ctx).ExpandRewrites(), + } +} + +func (e *Engine) BuildTree(ctx context.Context, subSet *relationtuple.SubjectSet, restDepth int) (t *relationtuple.Tree, err error) { + ctx, span := e.d.Tracer(ctx).Tracer().Start(ctx, "Engine.BuildTree") + defer otelx.End(span, &err) + + namespaceManager, err := e.d.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + // global max-depth takes precedence when it is the lesser or if the request max-depth is less than or equal to 0 + if globalMaxDepth := e.d.Config(ctx).MaxReadDepth(); restDepth <= 0 || globalMaxDepth < restDepth { + restDepth = globalMaxDepth + } + + b := e.newTreeBuilder(ctx, namespaceManager) + + if b.isStrict { + ns, _ := b.namespaceManager.GetNamespaceByName(ctx, subSet.Namespace) + rel := ns.FindRelation(subSet.Relation) + if rel == nil || (len(rel.Types) == 0 && rel.SubjectSetRewrite == nil) { + return nil, nil + } + } + + t, err = b.buildTreeRecursive(ctx, subSet, restDepth) + if err == nil { + span.AddEvent(events.NewPermissionsExpanded(ctx)) + } + return +} + +func (b *treeBuilder) buildTreeRecursive(ctx context.Context, subject relationtuple.Subject, restDepth int) (*relationtuple.Tree, error) { + subSet, isSubjectSet := subject.(*relationtuple.SubjectSet) + if !isSubjectSet || subSet.Relation == "" { + b.nodeCount++ + return newNode(ketoapi.TreeNodeLeaf, subject), nil + } + + // Cycle detection: if this subject is already being expanded in the current + // call path (DFS ancestry), we have a cycle. + if _, exists := b.visited[*subSet]; exists { + return &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: subject, + Truncation: &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonCycle, + }, + }, nil + } + b.visited[*subSet] = struct{}{} + defer delete(b.visited, *subSet) + + ns, _ := b.namespaceManager.GetNamespaceByName(ctx, subSet.Namespace) + rel := ns.FindRelation(subSet.Relation) + + if rel == nil { + if b.isStrict { + return nil, nil + } + // unknown relation, but we're not in strict mode, so we'll try to build it directly + return b.buildTreeDirect(ctx, subSet, nil, nil, restDepth) + } + + if rel.SubjectSetRewrite != nil { + if b.expandRewrites { + return b.buildTreeForRewrite(ctx, subSet, rel.SubjectSetRewrite, restDepth) + } + // it is a rewrite, but we don't expand rewrites + // checking direct doesn't make much sense, but that's what we currently do so we keep that behavior + return b.buildTreeDirect(ctx, subSet, rel, nil, restDepth) + } + + // rel is not a "permits" but "related", so we check directly + return b.buildTreeDirect(ctx, subSet, rel, nil, restDepth) +} + +// buildTreeDirect expands by querying the relation directly, without an OPL rewrites. +func (b *treeBuilder) buildTreeDirect(ctx context.Context, subSet *relationtuple.SubjectSet, rel *ast.Relation, resume *relationtuple.ExpandCursor, restDepth int) (*relationtuple.Tree, error) { + tree := newNode(ketoapi.TreeNodeUnion, subSet) + + directResume := func(pageToken keysetpagination.PageToken) *relationtuple.ExpandCursor { + return &relationtuple.ExpandCursor{Kind: relationtuple.ExpandCursorKindDirect, SubjectSet: subSet, NextPageToken: pageToken} + } + + if b.isMaxNodeLimit() { + tree.Truncation = tupleLimitTruncation(directResume(keysetpagination.PageToken{})) + return tree, nil + } + if restDepth <= 1 { + tree.Truncation = depthLimitTruncation(directResume(keysetpagination.PageToken{})) + return tree, nil + } + + for nextPage, _ := keysetpagination.NewPaginator(); !nextPage.IsLast(); { + var tuples []*relationtuple.RelationTuple + var err error + tuples, nextPage, err = b.d.RelationTupleManager().GetRelationTuples(ctx, &relationtuple.RelationQuery{ + Namespace: &subSet.Namespace, + Object: &subSet.Object, + Relation: &subSet.Relation, + }, + append(nextPage.ToOptions(), paginatorOptionsFromResume(resume)...)..., + ) + if err != nil { + return nil, err + } else if len(tuples) == 0 { + break + } + + pageStart := len(tree.Children) + for _, tuple := range tuples { + if b.isStrict && rel != nil { // if b.isStrict then rel != nil as we check that before calling this function, but let's better not rely on that invariant + ss, ok := tuple.Subject.(*relationtuple.SubjectSet) + if !ok || !slices.ContainsFunc(rel.Types, func(rt ast.RelationType) bool { + return rt.Namespace == ss.Namespace && rt.Relation == ss.Relation + }) { + continue + } + } + + childTree := newNode(ketoapi.TreeNodeLeaf, tuple.Subject) + if tupleSubSet, ok := tuple.Subject.(*relationtuple.SubjectSet); ok && tupleSubSet.Relation != "" { + childTree.Type = ketoapi.TreeNodeUnion + } + + b.nodeCount++ + tree.Children = append(tree.Children, childTree) + } + + if b.isMaxNodeLimit() && !nextPage.IsLast() { + tree.Truncation = tupleLimitTruncation(directResume(nextPage.PageToken())) + } + + // expand all the non-leaf nodes founds in this page + for _, child := range tree.Children[pageStart:] { + if child.Type != ketoapi.TreeNodeLeaf { + updated, err := b.buildTreeRecursive(ctx, child.Subject, restDepth-1) + if err != nil { + return nil, err + } + *child = *updated + } + } + + if tree.Truncation != nil { + break + } + } + + return tree, nil +} + +// buildTreeForRewrite expands a subject set using an OPL-defined rewrite rule. +// OPL rewrite children are structural (never paginated); only data nodes (direct +// relation tuples fetched from the DB) can be paginated. +func (b *treeBuilder) buildTreeForRewrite(ctx context.Context, subSet *relationtuple.SubjectSet, rewrite ast.Child, restDepth int) (*relationtuple.Tree, error) { + switch rewrite := rewrite.(type) { + case *ast.SubjectSetRewrite: + tree := newNode(ketoapi.TreeNodeUnion, subSet) + if rewrite.Operation == ast.OperatorAnd { + tree.Type = ketoapi.TreeNodeIntersection + } + + children := slices.SortedFunc(slices.Values(rewrite.Children), func(a, b ast.Child) int { + return rewriteCostOrder(a) - rewriteCostOrder(b) + }) + + for _, childRewrite := range children { + childTree, err := b.buildTreeForRewrite(ctx, subSet, childRewrite, restDepth) + if err != nil { + return nil, err + } + + // SubjectSetRewrite followed by another SubjectSetRewrite means compound expression; + // The subject of the 2nd rewrite is not meaningful at that point. + // ex: view = A AND (B OR C) -> (B OR C) has no meaningul subject + if childTree != nil { + if _, ok := childRewrite.(*ast.SubjectSetRewrite); ok { + childTree.Subject = nil + } + tree.Children = append(tree.Children, childTree) + } + } + return tree, nil + + case *ast.ComputedSubjectSet: + return b.buildTreeRecursive(ctx, &relationtuple.SubjectSet{ + Namespace: subSet.Namespace, + Object: subSet.Object, + Relation: rewrite.Relation, + }, restDepth-1) + case *ast.TupleToSubjectSet: + return b.buildTreeTupleToSubjectSet(ctx, subSet, rewrite, nil, restDepth) + case *ast.InvertResult: + // inversion never has a meaningful Subject + invertNode := newNode(ketoapi.TreeNodeExclusion, nil) + childTree, err := b.buildTreeForRewrite(ctx, subSet, rewrite.Child, restDepth) + if err != nil { + return nil, err + } + + if childTree != nil { + // any compound operation under an InvertResult also has no meaningful subject + // ex: view = !(A OR B) + if _, ok := rewrite.Child.(*ast.SubjectSetRewrite); ok { + childTree.Subject = nil + } + invertNode.Children = []*relationtuple.Tree{childTree} + } + return invertNode, nil + } + return nil, nil +} + +// buildTreeTupleToSubjectSet finds subject sets under subSet#child.Relation, and for each, +// recurses into that subject set's child.ComputedSubjectSetRelation. +func (b *treeBuilder) buildTreeTupleToSubjectSet(ctx context.Context, subSet *relationtuple.SubjectSet, rewrite *ast.TupleToSubjectSet, resume *relationtuple.ExpandCursor, restDepth int) (*relationtuple.Tree, error) { + treeSubject := &relationtuple.SubjectSet{ + Namespace: subSet.Namespace, + Object: subSet.Object, + Relation: rewrite.Relation, + } + tree := newNode(ketoapi.TreeNodeUnion, treeSubject) + ttuResume := func(pageToken keysetpagination.PageToken) *relationtuple.ExpandCursor { + return &relationtuple.ExpandCursor{Kind: relationtuple.ExpandCursorKindTTU, SubjectSet: treeSubject, TraverseRelation: &rewrite.ComputedSubjectSetRelation, NextPageToken: pageToken} + } + + if b.isMaxNodeLimit() { + tree.Truncation = tupleLimitTruncation(ttuResume(keysetpagination.PageToken{})) + return tree, nil + } + if restDepth <= 1 { + tree.Truncation = depthLimitTruncation(ttuResume(keysetpagination.PageToken{})) + return tree, nil + } + + for nextPage, _ := keysetpagination.NewPaginator(); !nextPage.IsLast(); { + var tuples []*relationtuple.RelationTuple + var err error + tuples, nextPage, err = b.d.RelationTupleManager().GetRelationTuples(ctx, &relationtuple.RelationQuery{ + Namespace: &subSet.Namespace, + Object: &subSet.Object, + Relation: &rewrite.Relation, + }, + append(nextPage.ToOptions(), paginatorOptionsFromResume(resume)...)..., + ) + if err != nil { + return nil, err + } else if len(tuples) == 0 { + break + } + + var toWorkOn []*relationtuple.RelationTuple + for _, tuple := range tuples { + _, ok := tuple.Subject.(*relationtuple.SubjectSet) + if !ok { + continue + } + + b.nodeCount++ + toWorkOn = append(toWorkOn, tuple) + } + + if b.isMaxNodeLimit() && !nextPage.IsLast() { + tree.Truncation = tupleLimitTruncation(ttuResume(nextPage.PageToken())) + } + + for _, tuple := range toWorkOn { + tupleSubSet := tuple.Subject.(*relationtuple.SubjectSet) + + childTree, err := b.buildTreeRecursive(ctx, &relationtuple.SubjectSet{ + Namespace: tupleSubSet.Namespace, + Object: tupleSubSet.Object, + Relation: rewrite.ComputedSubjectSetRelation, + }, restDepth-1) + if err != nil { + return nil, err + } + if childTree != nil { + tree.Children = append(tree.Children, childTree) + } + } + + if tree.Truncation != nil { + break + } + } + return tree, nil +} + +func tupleLimitTruncation(resume *relationtuple.ExpandCursor) *relationtuple.Truncation { + return &relationtuple.Truncation{Reason: relationtuple.TruncationReasonTupleLimit, Cursor: resume} +} + +func depthLimitTruncation(resume *relationtuple.ExpandCursor) *relationtuple.Truncation { + return &relationtuple.Truncation{Reason: relationtuple.TruncationReasonDepthLimit, Cursor: resume} +} + +func paginatorOptionsFromResume(resume *relationtuple.ExpandCursor) []keysetpagination.Option { + if resume != nil { + return []keysetpagination.Option{keysetpagination.WithToken(resume.NextPageToken)} + } + return nil +} + +func newNode(typ ketoapi.TreeNodeType, subSet relationtuple.Subject) *relationtuple.Tree { + return &relationtuple.Tree{ + Type: typ, + Subject: subSet, + } +} + +func (b *treeBuilder) isMaxNodeLimit() bool { + return b.nodeCount >= b.maxNodes +} + +// rewriteCostOrder returns a sort key for rewrite children so that cheaper, +// direct lookups are evaluated before more expensive traversals. +func rewriteCostOrder(c ast.Child) int { + switch c.(type) { + case *ast.ComputedSubjectSet: + return 0 + case *ast.TupleToSubjectSet: + return 1 + case *ast.SubjectSetRewrite: + return 2 + case *ast.InvertResult: + return 3 + default: + return 4 + } +} diff --git a/internal/expand/engine_test.go b/internal/expand/engine_test.go new file mode 100644 index 0000000..c021bcc --- /dev/null +++ b/internal/expand/engine_test.go @@ -0,0 +1,1377 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand_test + +import ( + "context" + "slices" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/internal/x" + "github.com/ory/keto/ketoapi" + "github.com/ory/keto/schema" +) + +func TestEngine(t *testing.T) { + emptyNs := driver.WithNamespaces([]*namespace.Namespace{{}}) + + t.Run("case=returns SubjectID on expand", func(t *testing.T) { + groupObj := uuid.Must(uuid.NewV4()) + tuple := &relationtuple.RelationTuple{ + Namespace: "Group", + Object: groupObj, + Relation: "rel", + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + } + + reg := driver.NewSqliteTestRegistry(t) + e := expand.NewEngine(reg) + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), tuple)) + + t.Run(`tuple does not exist, but ns:obj exists`, func(t *testing.T) { + tree, err := e.BuildTree(context.Background(), &relationtuple.SubjectSet{ + Namespace: "Group", + Object: groupObj, + Relation: "rel2", + }, 100) + require.NoError(t, err) + + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: "Group", + Object: groupObj, + Relation: "rel2", + }, + Children: nil, + } + assert.Equal(t, expectedTree, tree) + }) + + t.Run(`no such ns:obj exists at all`, func(t *testing.T) { + tree, err := e.BuildTree(context.Background(), &relationtuple.SubjectSet{ + Namespace: "Folder", + Object: groupObj, + Relation: "rel", + }, 100) + require.NoError(t, err) + + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: "Folder", + Object: groupObj, + Relation: "rel", + }, + Children: nil, + } + assert.Equal(t, expectedTree, tree) + }) + }) + + t.Run("case=expands one level", func(t *testing.T) { + tommy := &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())} + paul := &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())} + boulderGroup := uuid.Must(uuid.NewV4()) + bouldererUserSet := &relationtuple.SubjectSet{ + Relation: "member", + Object: boulderGroup, + } + boulderers := []*relationtuple.RelationTuple{ + { + Relation: bouldererUserSet.Relation, + Object: boulderGroup, + Subject: tommy, + }, + { + Relation: bouldererUserSet.Relation, + Object: boulderGroup, + Subject: paul, + }, + } + reg := driver.NewSqliteTestRegistry(t, emptyNs) + e := expand.NewEngine(reg) + + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), boulderers...)) + + tree, err := e.BuildTree(context.Background(), bouldererUserSet, 100) + require.NoError(t, err) + + require.True(t, expand.AssertInternalTreesAreEqual(t, &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: bouldererUserSet, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: paul, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: tommy, + }, + }, + }, tree)) + }) + + t.Run("case=expands two levels", func(t *testing.T) { + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: uuid.Must(uuid.NewV4()), + Relation: "transitive member", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: uuid.Must(uuid.NewV4()), + Relation: "member", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + }, + }, + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: uuid.Must(uuid.NewV4()), + Relation: "member", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + }, + }, + }, + } + + reg := driver.NewSqliteTestRegistry(t, emptyNs) + e := expand.NewEngine(reg) + + for _, group := range expectedTree.Children { + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &relationtuple.RelationTuple{ + Object: expectedTree.Subject.(*relationtuple.SubjectSet).Object, + Relation: "transitive member", + Subject: &relationtuple.SubjectSet{ + Object: group.Subject.(*relationtuple.SubjectSet).Object, + Relation: "member", + }, + })) + + for _, user := range group.Children { + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &relationtuple.RelationTuple{ + Object: group.Subject.(*relationtuple.SubjectSet).Object, + Relation: "member", + Subject: user.Subject.(*relationtuple.SubjectID), + })) + } + } + + actualTree, err := e.BuildTree(context.Background(), expectedTree.Subject.(*relationtuple.SubjectSet), 100) + require.NoError(t, err) + require.True(t, expand.AssertInternalTreesAreEqual(t, expectedTree, actualTree)) + }) + + t.Run("case=respects max depth", func(t *testing.T) { + reg := driver.NewSqliteTestRegistry(t, emptyNs) + e := expand.NewEngine(reg) + + ids := x.UUIDs(5) + for i := 1; i < len(ids); i++ { + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &relationtuple.RelationTuple{ + Object: ids[i-1], + Relation: "child", + Subject: &relationtuple.SubjectSet{ + Object: ids[i], + Relation: "child", + }, + })) + } + + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: ids[0], + Relation: "child", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: ids[1], + Relation: "child", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: ids[2], + Relation: "child", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: ids[3], + Relation: "child", + }, + Truncation: &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonDepthLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: &relationtuple.SubjectSet{ + Object: ids[3], + Relation: "child", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + actualTree, err := e.BuildTree(context.Background(), expectedTree.Subject.(*relationtuple.SubjectSet), 4) + require.NoError(t, err) + + assert.Equal(t, expectedTree, actualTree) + }) + + t.Run("case=paginates", func(t *testing.T) { + innerReg := driver.NewSqliteTestRegistry(t, emptyNs) + reg, mw := testhelpers.RegistryWithManagerWrapper(t, innerReg, keysetpagination.WithSize(2)) + + e := expand.NewEngine(reg) + root := uuid.Must(uuid.NewV4()) + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{Object: root, Relation: "access"}, + } + + for _, user := range x.UUIDs(4) { + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &relationtuple.RelationTuple{ + Object: root, + Relation: "access", + Subject: &relationtuple.SubjectID{ID: user}, + })) + expectedTree.Children = append(expectedTree.Children, &relationtuple.Tree{ + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: user}, + }) + } + + tree, err := e.BuildTree(context.Background(), &relationtuple.SubjectSet{ + Object: root, + Relation: "access", + }, 10) + require.NoError(t, err) + + assert.True(t, expand.AssertInternalTreesAreEqual(t, expectedTree, tree)) + assert.Len(t, mw.RequestedPages, 2) + }) + + t.Run("case=handles subject sets as leaf", func(t *testing.T) { + reg := driver.NewSqliteTestRegistry(t, emptyNs) + e := expand.NewEngine(reg) + + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: uuid.Must(uuid.NewV4()), + Relation: "rel", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Object: uuid.Must(uuid.NewV4()), + Relation: "sr", + }, + }, + }, + } + + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), &relationtuple.RelationTuple{ + Object: expectedTree.Subject.(*relationtuple.SubjectSet).Object, + Relation: expectedTree.Subject.(*relationtuple.SubjectSet).Relation, + Subject: expectedTree.Children[0].Subject, + })) + + tree, err := e.BuildTree(context.Background(), expectedTree.Subject.(*relationtuple.SubjectSet), 100) + require.NoError(t, err) + assert.Equal(t, expectedTree, tree) + }) + + t.Run("case=circular tuples", func(t *testing.T) { + sendlingerTor, odeonsplatz, centralStation, connected, namesp := uuid.NewV5(uuid.Nil, "Sendlinger Tor"), uuid.NewV5(uuid.Nil, "Odeonsplatz"), uuid.NewV5(uuid.Nil, "Central Station"), "connected", "92384" + + sendlingerTorSS, odeonsplatzSS, centralStationSS := &relationtuple.SubjectSet{ + Namespace: namesp, + Object: sendlingerTor, + Relation: connected, + }, &relationtuple.SubjectSet{ + Namespace: namesp, + Object: odeonsplatz, + Relation: connected, + }, &relationtuple.SubjectSet{ + Namespace: namesp, + Object: centralStation, + Relation: connected, + } + + reg := driver.NewSqliteTestRegistry(t, driver.WithNamespaces([]*namespace.Namespace{{Name: namesp}})) + e := expand.NewEngine(reg) + + expectedTree := &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: sendlingerTorSS, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: odeonsplatzSS, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: centralStationSS, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: sendlingerTorSS, + Truncation: &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonCycle, + }, + }, + }, + }, + }, + }, + }, + } + + require.NoError(t, reg.RelationTupleManager().WriteRelationTuples(context.Background(), []*relationtuple.RelationTuple{ + { + Namespace: namesp, + Object: sendlingerTor, + Relation: connected, + Subject: odeonsplatzSS, + }, + { + Namespace: namesp, + Object: odeonsplatz, + Relation: connected, + Subject: centralStationSS, + }, + { + Namespace: namesp, + Object: centralStation, + Relation: connected, + Subject: sendlingerTorSS, + }, + }...)) + + tree, err := e.BuildTree(context.Background(), &relationtuple.SubjectSet{ + Namespace: namesp, + Object: sendlingerTor, + Relation: connected, + }, 100) + + require.NoError(t, err) + assert.Equal(t, expectedTree, tree) + }) + + t.Run("case=returns result on unknown subject", func(t *testing.T) { + reg := driver.NewSqliteTestRegistry(t, emptyNs) + e := expand.NewEngine(reg) + ss := &relationtuple.SubjectSet{ + Namespace: "unknown", + Object: uuid.Must(uuid.NewV4()), + Relation: "rel", + } + tree, err := e.BuildTree(context.Background(), ss, 100) + require.NoError(t, err) + assert.Equal(t, &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: ss, + }, tree) + }) +} + +func TestEngineOpl(t *testing.T) { + type expandTest struct { + name string + opl string + inputTuples []string + expandInput string + expected *relationtuple.Tree + strict bool + maxNodes int + only bool + } + tests := []expandTest{ + { + name: "case=Multile levels of union and exclusion", + strict: true, + opl: ` + class User implements Namespace {} + class Corporate implements Namespace { + related: { + releasers: User[] + } + } + class UserGroup implements Namespace { + related: { + members: User[] + } + } + class AccountGroup implements Namespace { + related: { + managers: SubjectSet[] + } + } + class Account implements Namespace { + related: { + managers: SubjectSet[] + } + } + class Task implements Namespace { + related: { + managers: (SubjectSet | SubjectSet)[] + + blocklist: SubjectSet[] + } + permits = { + viewer: (ctx: Context) => this.related.managers.includes(ctx.subject) && !this.related.blocklist.includes(ctx.subject) + } + } + `, + + inputTuples: []string{ + "UserGroup:UG1#members@User:1", + "UserGroup:UG1#members@User:2", + "UserGroup:UG2#members@User:2", + "UserGroup:UG2#members@User:3", + "UserGroup:UG2#members@User:4", + "UserGroup:UG3#members@User:4", + "UserGroup:UG3#members@User:5", + + "AccountGroup:AG1#managers@UserGroup:UG1#members", + "AccountGroup:AG1#managers@UserGroup:UG2#members", + "AccountGroup:AG2#managers@UserGroup:UG3#members", + + "Account:A1#managers@AccountGroup:AG1#managers", + "Account:A1#managers@AccountGroup:AG2#managers", + + "Task:T1#managers@Account:A1#managers", + + // thru corporate + "Task:T1#managers@Corporate:C1#releasers", + + // blocklist + "Task:T1#blocklist@UserGroup:UG-1#members", + "UserGroup:UG-1#members@User:5", + }, + expandInput: "Task:T1#viewer", + maxNodes: 20, + expected: treeNode(t, intersection, "Task:T1#viewer", nil, + treeNode(t, union, "Task:T1#managers", nil, + treeNode(t, union, "Account:A1#managers", nil, + treeNode(t, union, "AccountGroup:AG1#managers", nil, + treeNode(t, union, "UserGroup:UG1#members", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + ), + treeNode(t, union, "UserGroup:UG2#members", nil, + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + treeNode(t, leaf, "User:4", nil), + ), + ), + treeNode(t, union, "AccountGroup:AG2#managers", nil, + treeNode(t, union, "UserGroup:UG3#members", nil, + treeNode(t, leaf, "User:4", nil), + treeNode(t, leaf, "User:5", nil), + ), + ), + ), + treeNode(t, union, "Corporate:C1#releasers", nil), + ), + treeNode(t, exclusion, "", nil, + treeNode(t, union, "Task:T1#blocklist", nil, + treeNode(t, union, "UserGroup:UG-1#members", nil, + treeNode(t, leaf, "User:5", nil), + ), + ), + ), + ), + }, + { + name: "case=expands 1 level without OPL", + opl: "", + inputTuples: []string{ + "Group:parent#users@User:1", + "Group:parent#users@User:2", + "Group:parent#users@User:3", + "Group:parent#users@User4", + "Group:parent#users@ApiKey:x", + }, + expandInput: "Group:parent#users", + expected: treeNode(t, union, "Group:parent#users", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + treeNode(t, leaf, "User4", nil), + treeNode(t, leaf, "ApiKey:x", nil), + ), + }, + { + name: "case=expands simple ComputedSubjectSet level with OPL", + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + }, + { + name: "case=expands simple Inverted ComputedSubjectSet level with OPL", + strict: true, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + permits = { + view: (ctx: Context) => !this.related.viewers.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, exclusion, "", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + ), + }, + { + name: "case=expands blocklistexample", + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + blocklist: User[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) && !this.related.blocklist.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#blocklist@User:1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, intersection, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + ), + treeNode(t, exclusion, "", nil, + treeNode(t, union, "File:f1#blocklist", nil, + treeNode(t, leaf, "User:1", nil), + ), + ), + ), + }, + { + name: "case=expands 2 blocklists example", + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + blocklist1: User[] + blocklist2: User[] + } + permits = { + view: (ctx: Context) => + this.related.viewers.includes(ctx.subject) && + !( + this.related.blocklist1.includes(ctx.subject) && + this.related.blocklist2.includes(ctx.subject) + ) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#blocklist1@User:1", + "File:f1#blocklist2@User:2", + }, + expandInput: "File:f1#view", + expected: treeNode(t, intersection, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + ), + treeNode(t, exclusion, "", nil, + treeNode(t, intersection, "", nil, + treeNode(t, union, "File:f1#blocklist2", nil, + treeNode(t, leaf, "User:2", nil), + ), + treeNode(t, union, "File:f1#blocklist1", nil, + treeNode(t, leaf, "User:1", nil), + ), + ), + ), + ), + }, + { + name: "case=expands (includes or traverse)", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || + this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "Group:g1#members@User:4", + "File:f1#viewerGroups@Group:g1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + treeNode(t, union, "File:f1#viewerGroups", nil, + treeNode(t, union, "Group:g1#members", nil, + treeNode(t, leaf, "User:4", nil), + ), + ), + ), + }, + { + name: "case=expands (includes or traverse) - traverse group has no member tuples", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) || + this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "File:f1#viewerGroups@Group:g1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + treeNode(t, union, "File:f1#viewerGroups", nil, + treeNode(t, union, "Group:g1#members", nil), + ), + ), + }, + { + name: "case=expands (includes and traverse) - traverse has no tuples", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + view: (ctx: Context) => this.related.viewers.includes(ctx.subject) && + this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + }, + expandInput: "File:f1#view", + expected: treeNode(t, intersection, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + treeNode(t, union, "File:f1#viewerGroups", nil), + ), + }, + { + name: "case=expands multiple child permits", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + viewUser: (ctx: Context) => this.related.viewers.includes(ctx.subject), + viewGroup: (ctx: Context) => this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)), + + view: (ctx: Context) => this.permits.viewUser(ctx) && this.permits.viewGroup(ctx) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "Group:g1#members@User:4", + "File:f1#viewerGroups@Group:g1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, intersection, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewUser", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + treeNode(t, union, "File:f1#viewGroup", nil, + treeNode(t, union, "File:f1#viewerGroups", nil, + treeNode(t, union, "Group:g1#members", nil, + treeNode(t, leaf, "User:4", nil), + ), + ), + ), + ), + }, + { + name: "case=expands compound relations", + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + editors: User[] + owners: User[] + } + permits = { + view: (ctx: Context) => + ( + this.related.viewers.includes(ctx.subject) || + this.related.editors.includes(ctx.subject) + ) && + this.related.owners.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#editors@User:2", + "File:f1#editors@User:3", + "File:f1#owners@User:3", + }, + expandInput: "File:f1#view", + expected: treeNode(t, intersection, "File:f1#view", nil, + treeNode(t, union, "File:f1#owners", nil, + treeNode(t, leaf, "User:3", nil), + ), + treeNode(t, union, "", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + ), + treeNode(t, union, "File:f1#editors", nil, + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + ), + }, + { + name: "case=expands repeated child permit calls", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + viewUser: (ctx: Context) => this.related.viewers.includes(ctx.subject), + viewGroup: (ctx: Context) => this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)), + + view: (ctx: Context) => ( + ( this.permits.viewUser(ctx) && this.permits.viewGroup(ctx) ) + || + ( this.permits.viewUser(ctx) || this.permits.viewGroup(ctx) ) + ) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "Group:g1#members@User:4", + "File:f1#viewerGroups@Group:g1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, intersection, "", nil, + treeNode(t, union, "File:f1#viewUser", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + treeNode(t, union, "File:f1#viewGroup", nil, + treeNode(t, union, "File:f1#viewerGroups", nil, + treeNode(t, union, "Group:g1#members", nil, + treeNode(t, leaf, "User:4", nil), + ), + ), + ), + ), + treeNode(t, union, "File:f1#viewUser", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + treeNode(t, union, "File:f1#viewGroup", nil, + treeNode(t, union, "File:f1#viewerGroups", nil, + treeNode(t, union, "Group:g1#members", nil, + treeNode(t, leaf, "User:4", nil), + ), + ), + ), + ), + }, + { + name: "case=expands recursive groups should detect circular references", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + parent: Group[] + } + permits = { + isMember: (ctx: Context) => this.related.members.includes(ctx.subject) || + this.related.parent.traverse(g => g.permits.isMember(ctx)) + } + } + `, + inputTuples: []string{ + "Group:g1#members@User:4", + "Group:g1#parent@Group:g2", + "Group:g2#members@User:5", + "Group:g2#parent@Group:g1", + }, + expandInput: "Group:g1#isMember", + expected: treeNode(t, union, "Group:g1#isMember", nil, + treeNode(t, union, "Group:g1#members", nil, + treeNode(t, leaf, "User:4", nil), + ), + treeNode(t, union, "Group:g1#parent", nil, + treeNode(t, union, "Group:g2#isMember", nil, + treeNode(t, union, "Group:g2#members", nil, + treeNode(t, leaf, "User:5", nil), + ), + treeNode(t, union, "Group:g2#parent", nil, + treeNode(t, union, "Group:g1#isMember", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonCycle, + }), + ), + ), + ), + ), + }, + { + // node 1.= Group:g2#members + // node 2 = Group:g3#members + // rest is truncated. + name: "case=node-limit is counted correctly for SubjectSet relationships", + maxNodes: 2, + inputTuples: []string{ + "Group:g1#members@Group:g2#members", + "Group:g2#members@Group:g3#members", + "Group:g3#members@User:1", + }, + expandInput: "Group:g1#members", + expected: treeNode(t, union, "Group:g1#members", nil, + treeNode(t, union, "Group:g2#members", nil, + treeNode(t, union, "Group:g3#members", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: testhelpers.SubjectSetFromString(t, "Group:g3#members"), + }, + }), + ), + ), + }, + { + name: "case=stops at empty SubjectSet", + inputTuples: []string{ + "Group:g1#members@Group:g2#members", + }, + expandInput: "Group:g1#members", + expected: treeNode(t, union, "Group:g1#members", nil, + treeNode(t, union, "Group:g2#members", nil), + ), + }, + { + name: "case=node limit does not block OPL structural traversal when there's data", + maxNodes: 3, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + } + } + class File implements Namespace{ + related: { + viewers: User[] + viewerGroups: Group[] + } + permits = { + viewUser: (ctx: Context) => this.related.viewers.includes(ctx.subject), + viewGroup: (ctx: Context) => this.related.viewerGroups.traverse(g => g.related.members.includes(ctx.subject)), + view: (ctx: Context) => this.permits.viewUser(ctx) || this.permits.viewGroup(ctx) + } + } + `, + inputTuples: []string{ + "File:f1#viewers@User:1", + "File:f1#viewers@User:2", + "File:f1#viewers@User:3", + + "File:f1#viewerGroups@Group:g1", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, union, "File:f1#viewUser", nil, + treeNode(t, union, "File:f1#viewers", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + treeNode(t, leaf, "User:3", nil), + ), + ), + treeNode(t, union, "File:f1#viewGroup", nil, + treeNode(t, union, "File:f1#viewerGroups", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindTTU, + SubjectSet: testhelpers.SubjectSetFromString(t, "File:f1#viewerGroups"), + TraverseRelation: new("members"), + }, + }), + ), + ), + }, + { + name: "case=unknown 2nd relationship on strict mode", + strict: true, + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + viewers: User[] + } + } + class File implements Namespace{ + related: { + groupViewers: SubjectSet[] + groups: Group[] + } + permits = { + view: (ctx: Context) => this.related.groupViewers.includes(ctx.subject) || + || this.related.groups.traverse(g => g.related.viewers.includes(ctx.subject)) + } + } + `, + inputTuples: []string{ + "Group:g1#editors@User:1", // "editor" relationship does not exist on Group + "Group:g2#editors@User:2", + + "File:f1#groupViewers@Group:g1#readers", // "reader" relationship does not exist on Group + "File:f1#groups@Group:g2", + }, + expandInput: "File:f1#view", + expected: treeNode(t, union, "File:f1#view", nil, + treeNode(t, union, "File:f1#groupViewers", nil), + treeNode(t, union, "File:f1#groups", nil, + treeNode(t, union, "Group:g2#viewers", nil), + ), + ), + }, + { + name: "case=unknown relationship on strict mode", + maxNodes: 3, + strict: true, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + } + `, + inputTuples: []string{ + "File:f1#undefined@User:1", + }, + expandInput: "File:f1#undefined", + expected: nil, + }, + { + name: "case=unknown namespace on strict mode", + maxNodes: 3, + strict: true, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + } + `, + inputTuples: []string{ + "File:f1#undefined@User:1", + }, + expandInput: "File:f1#undefined", + expected: nil, + }, + { + name: "case=unknown relationship on non-strict mode", + maxNodes: 3, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + } + `, + inputTuples: []string{ + "File:f1#undefined@User:1", + }, + expandInput: "File:f1#undefined", + expected: treeNode(t, union, "File:f1#undefined", nil, + treeNode(t, leaf, "User:1", nil), + ), + }, + { + name: "case=unknown namespace on non-strict mode", + maxNodes: 3, + opl: ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + } + } + `, + inputTuples: []string{}, + expandInput: "X:f1#undefined", + expected: treeNode(t, union, "X:f1#undefined", nil), + }, + { + name: "case=limit should be applied and prevent nesting", + strict: true, + opl: ` + class File implements Namespace{} + class Folder implements Namespace{ + related: { + members: (File | SubjectSet)[] + } + } + `, + // the 1st digit refers to the depth, 2nd digit refers to the position in that depth. + inputTuples: []string{ + "Folder:11#members@Folder:21#members", + "Folder:11#members@Folder:22#members", + + "Folder:21#members@Folder:31#members", + "Folder:21#members@Folder:32#members", + "Folder:21#members@Folder:33#members", + + // Folder:22 has no members + + "Folder:31#members@File:41", "Folder:31#members@File:42", + "Folder:32#members@File:43", "Folder:32#members@File:44", + }, + maxNodes: 4, + expandInput: "Folder:11#members", + expected: treeNode(t, union, "Folder:11#members", nil, + treeNode(t, union, "Folder:21#members", nil, + treeNode(t, union, "Folder:31#members", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: testhelpers.SubjectSetFromString(t, "Folder:31#members"), + }, + }), + treeNode(t, union, "Folder:32#members", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: testhelpers.SubjectSetFromString(t, "Folder:32#members"), + }, + }), + treeNode(t, union, "Folder:33#members", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: testhelpers.SubjectSetFromString(t, "Folder:33#members"), + }, + }), + ), + treeNode(t, union, "Folder:22#members", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindDirect, + SubjectSet: testhelpers.SubjectSetFromString(t, "Folder:22#members"), + }, + }), + ), + }, + { + name: "case=direct relations should be prioritized over rewrites", + opl: ` + class User implements Namespace{} + class Group implements Namespace{ + related: { + members: User[] + parent: Group[] + hiddenMembers: User[] + } + permits = { + isMember: (ctx: Context) => this.related.members.includes(ctx.subject) + || this.related.parent.traverse(p => p.permits.isMember(ctx)) + || this.related.hiddenMembers.includes(ctx.subject) + } + } + `, + inputTuples: []string{ + "Group:Sales#parent@Group:All", + + "Group:Sales#members@User:1", + "Group:Sales#members@User:2", + + "Group:Sales#hiddenMembers@User:3", + "Group:Sales#hiddenMembers@User:4", + + "Group:All#members@User:5", + "Group:All#hiddenMembers@User:6", + }, + maxNodes: 4, + expandInput: "Group:Sales#isMember", + expected: treeNode(t, union, "Group:Sales#isMember", nil, + treeNode(t, union, "Group:Sales#members", nil, + treeNode(t, leaf, "User:1", nil), + treeNode(t, leaf, "User:2", nil), + ), + treeNode(t, union, "Group:Sales#hiddenMembers", nil, + treeNode(t, leaf, "User:3", nil), + treeNode(t, leaf, "User:4", nil), + ), + treeNode(t, union, "Group:Sales#parent", &relationtuple.Truncation{ + Reason: relationtuple.TruncationReasonTupleLimit, + Cursor: &relationtuple.ExpandCursor{ + Kind: relationtuple.ExpandCursorKindTTU, + SubjectSet: testhelpers.SubjectSetFromString(t, "Group:Sales#parent"), + TraverseRelation: new("isMember"), + }, + }), + ), + }, + } + + t.Run("loop tests", func(t *testing.T) { + focused := slices.ContainsFunc(tests, func(tt expandTest) bool { return tt.only }) + for _, tt := range tests { + if focused && !tt.only { + continue + } + t.Run(tt.name, func(t *testing.T) { + if tt.opl != "" { + _, errs := schema.Parse(tt.opl) + require.Empty(t, errs) + } + opts := []driver.TestRegistryOption{ + driver.WithConfig(config.KeyFeatureFlagExpandRewrites, true), + driver.WithConfig(config.KeyLimitMaxReadDepth, 50), + } + + if tt.opl != "" { + opts = append(opts, driver.WithOPL(tt.opl)) + } else { + opts = append(opts, driver.WithConfig(config.KeyNamespaces, []*namespace.Namespace{})) + } + if tt.maxNodes > 0 { + opts = append(opts, driver.WithConfig(config.KeyLimitExpandMaxSize, tt.maxNodes)) + } + if tt.strict { + opts = append(opts, driver.WithConfig(config.KeyFeatureFlagStrictMode, tt.strict)) + } + + reg := driver.NewSqliteTestRegistry(t, opts...) + + e := expand.NewEngine(reg) + + ctx := context.Background() + testhelpers.MapAndInsertTuplesFromString(t, reg, tt.inputTuples) + + tree, err := e.BuildTree(ctx, testhelpers.SubjectSetFromString(t, tt.expandInput), 100) + require.NoError(t, err) + + expand.RequireInternalTreesAreEqual(t, tt.expected, tree) + }) + } + }) +} + +var ( + union = ketoapi.TreeNodeUnion + leaf = ketoapi.TreeNodeLeaf + intersection = ketoapi.TreeNodeIntersection + exclusion = ketoapi.TreeNodeExclusion +) + +// treeNode builds a *relationtuple.Tree for use in test expectations. +// Pass subject="" to leave Subject nil (needed for Exclusion/Intersection +// nodes that have no subject). Omit children to leave Children nil. +func treeNode(t testing.TB, nodeType ketoapi.TreeNodeType, subject string, truncation *relationtuple.Truncation, children ...*relationtuple.Tree) *relationtuple.Tree { + t.Helper() + node := &relationtuple.Tree{ + Type: nodeType, + Truncation: truncation, + Children: children, + } + if subject != "" { + node.Subject = testhelpers.SubjectFromString(t, subject) + } + if len(children) == 0 { + node.Children = nil + } + return node +} diff --git a/internal/expand/handler.go b/internal/expand/handler.go new file mode 100644 index 0000000..bdee716 --- /dev/null +++ b/internal/expand/handler.go @@ -0,0 +1,174 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand + +import ( + "context" + "net/http" + + "connectrpc.com/connect" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + + "github.com/ory/keto/ketoapi" + + "github.com/ory/herodot" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x" +) + +type ( + handlerDependencies interface { + EngineProvider + relationtuple.ManagerProvider + relationtuple.MapperProvider + logrusx.Provider + httpx.WriterProvider + x.HandlerOptionsProvider + } + Handler struct { + relationtuplesconnect.UnimplementedExpandServiceHandler + d handlerDependencies + } +) + +var ( + _ relationtuplesconnect.ExpandServiceHandler = (*Handler)(nil) + _ *expandPermissions = nil +) + +const RouteBase = "/relation-tuples/expand" + +func NewHandler(d handlerDependencies) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterReadRoutes(r *httprouterx.RouterPublic) { + r.GET(RouteBase, h.getExpand) + + expandHandler := connect.NewUnaryHandler( + relationtuplesconnect.ExpandServiceExpandProcedure, + h.Expand, + connect.WithSchema(rts.File_ory_keto_relation_tuples_v1alpha2_expand_service_proto. + Services().ByName("ExpandService"). + Methods().ByName("Expand")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.ExpandServiceExpandProcedure, expandHandler) +} + +func (h *Handler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{ + rts.File_ory_keto_relation_tuples_v1alpha2_expand_service_proto, + rts.File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto, + } +} + +// Expand Permissions Request Parameters +// +// swagger:parameters expandPermissions +type expandPermissions struct { + // in:query + MaxDepth int `json:"max-depth"` + // in:query + ketoapi.SubjectSet +} + +// swagger:route GET /relation-tuples/expand permission expandPermissions +// +// # Expand a Relationship into permissions. +// +// Use this endpoint to expand a relationship tuple into permissions. +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: expandedPermissionTree +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-medium +func (h *Handler) getExpand(w http.ResponseWriter, r *http.Request) { + maxDepth, err := x.GetMaxDepthFromQuery(r.URL.Query()) + if err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error())) + return + } + + subSet := (&ketoapi.SubjectSet{}).FromURLQuery(r.URL.Query()) + internal, err := h.d.ReadOnlyMapper().FromSubjectSet(r.Context(), subSet) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + res, err := h.d.ExpandEngine().BuildTree(r.Context(), internal, maxDepth) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + if res == nil { + h.d.Writer().Write(w, r, herodot.ErrNotFound().WithError("no relation tuple found")) + return + } + + tree, err := h.d.ReadOnlyMapper().ToTree(r.Context(), res) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Writer().Write(w, r, tree) +} + +func (h *Handler) Expand(ctx context.Context, req *connect.Request[rts.ExpandRequest]) (*connect.Response[rts.ExpandResponse], error) { + var subSet *ketoapi.SubjectSet + + switch sub := req.Msg.Subject.Ref.(type) { + case *rts.Subject_Id: + return connect.NewResponse(&rts.ExpandResponse{ + Tree: &rts.SubjectTree{ + NodeType: rts.NodeType_NODE_TYPE_LEAF, + Subject: rts.NewSubjectID(sub.Id), + }, + }), nil + case *rts.Subject_Set: + subSet = &ketoapi.SubjectSet{ + Namespace: sub.Set.Namespace, + Object: sub.Set.Object, + Relation: sub.Set.Relation, + } + } + + internal, err := h.d.ReadOnlyMapper().FromSubjectSet(ctx, subSet) + if err != nil { + return nil, err + } + res, err := h.d.ExpandEngine().BuildTree(ctx, internal, int(req.Msg.MaxDepth)) + if err != nil { + return nil, err + } + if res == nil { + return connect.NewResponse(&rts.ExpandResponse{}), nil + } + + tree, err := h.d.ReadOnlyMapper().ToTree(ctx, res) + if err != nil { + return nil, err + } + + return connect.NewResponse(&rts.ExpandResponse{Tree: tree.ToProto()}), nil +} diff --git a/internal/expand/handler_test.go b/internal/expand/handler_test.go new file mode 100644 index 0000000..adeb5ec --- /dev/null +++ b/internal/expand/handler_test.go @@ -0,0 +1,131 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand_test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/ory/x/httprouterx" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/expand" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/ketoapi" +) + +func TestRESTHandler(t *testing.T) { + nspace := &namespace.Namespace{ + Name: "expand handler", + } + + reg := driver.NewSqliteTestRegistry(t, driver.WithNamespaces([]*namespace.Namespace{nspace})) + h := expand.NewHandler(reg) + r := httprouterx.NewRouterPublic() + h.RegisterReadRoutes(r) + ts := httptest.NewServer(r) + defer ts.Close() + + t.Run("case=returns bad request on malformed int", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + expand.RouteBase + "?max-depth=foo") + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "invalid syntax") + }) + + t.Run("case=returns not found on unknown namespace", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + expand.RouteBase + "?" + url.Values{ + "max-depth": {"10"}, + "namespace": {"not " + nspace.Name}, + }.Encode()) + require.NoError(t, err) + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "Unknown namespace") + }) + + t.Run("case=returns not found on unknown namespace", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + expand.RouteBase + "?" + url.Values{ + "max-depth": {"10"}, + "namespace": {"not " + nspace.Name}, + }.Encode()) + require.NoError(t, err) + + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(body), "Unknown namespace") + }) + + t.Run("case=returns expand tree", func(t *testing.T) { + rootSub := &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "root", + Relation: "parent of", + } + expectedTree := &ketoapi.Tree[*ketoapi.RelationTuple]{ + Type: ketoapi.TreeNodeUnion, + Tuple: &ketoapi.RelationTuple{ + SubjectSet: rootSub, + }, + Children: []*ketoapi.Tree[*ketoapi.RelationTuple]{ + { + Type: ketoapi.TreeNodeLeaf, + Tuple: &ketoapi.RelationTuple{ + SubjectID: new("child0"), + }, + }, + { + Type: ketoapi.TreeNodeLeaf, + Tuple: &ketoapi.RelationTuple{ + SubjectID: new("child1"), + }, + }, + }, + } + + testhelpers.MapAndInsertTuples(t, reg, + &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: rootSub.Object, + Relation: rootSub.Relation, + SubjectID: expectedTree.Children[0].Tuple.SubjectID, + }, + &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: rootSub.Object, + Relation: rootSub.Relation, + SubjectID: expectedTree.Children[1].Tuple.SubjectID, + }, + ) + + qs := rootSub.ToURLQuery() + qs.Set("max-depth", "2") + resp, err := ts.Client().Get(ts.URL + expand.RouteBase + "?" + qs.Encode()) + require.NoError(t, err) + + require.Equal(t, http.StatusOK, resp.StatusCode) + + actualTree := ketoapi.Tree[*ketoapi.RelationTuple]{} + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + t.Logf("body: %s", string(body)) + require.NoError(t, json.NewDecoder(bytes.NewBuffer(body)).Decode(&actualTree)) + expand.AssertExternalTreesAreEqual(t, expectedTree, &actualTree) + }) +} diff --git a/internal/expand/testhelper.go b/internal/expand/testhelper.go new file mode 100644 index 0000000..dad125c --- /dev/null +++ b/internal/expand/testhelper.go @@ -0,0 +1,127 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package expand + +import ( + "fmt" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +func AssertExternalTreesAreEqual(t *testing.T, expected, actual *ketoapi.Tree[*ketoapi.RelationTuple]) { + t.Helper() + assert.Truef(t, treesAreEqual(t, expected, actual), + "expected:\n%+v\n\nactual:\n%+v", expected, actual) +} + +// TODO(hperl): Refactor to generic tree equality helper. +func treesAreEqual(t *testing.T, expected, actual *ketoapi.Tree[*ketoapi.RelationTuple]) bool { + if expected == nil || actual == nil { + return expected == actual + } + + if expected.Type != actual.Type { + t.Logf("expected type %q, actual type %q", expected.Type, actual.Type) + return false + } + if !assert.ObjectsAreEqual(expected.Tuple.SubjectID, actual.Tuple.SubjectID) || !assert.ObjectsAreEqual(expected.Tuple.SubjectSet, actual.Tuple.SubjectSet) { + t.Logf("expected subject: %+v %+v, actual subject: %+v %+v", expected.Tuple.SubjectID, expected.Tuple.SubjectSet, actual.Tuple.SubjectID, actual.Tuple.SubjectSet) + return false + } + if len(expected.Children) != len(actual.Children) { + t.Logf("expected len(children)=%d, actual len(children)=%d", len(expected.Children), len(actual.Children)) + return false + } + + // For children, we check for equality disregarding the order +outer: + for _, expectedChild := range expected.Children { + for _, actualChild := range actual.Children { + if treesAreEqual(t, expectedChild, actualChild) { + continue outer + } + } + t.Logf("expected child:\n%+v\n\nactual child:\n%+v", expectedChild, actual) + return false + } + return true +} + +// formatTree returns a human-readable, indented representation of a tree. +func formatTree(tree *relationtuple.Tree, indent string) string { + if tree == nil { + return indent + "\n" + } + var b strings.Builder + fmt.Fprintf(&b, "%s(%s) %+v", indent, tree.Type, tree.Subject) + if e := tree.Truncation; e != nil { + fmt.Fprintf(&b, " [truncation: reason=%s cursor=%s]", e.Reason, formatExpandCursor(e.Cursor)) + } + b.WriteByte('\n') + for _, child := range tree.Children { + b.WriteString(formatTree(child, indent+" ")) + } + return b.String() +} + +func formatExpandCursor(c *relationtuple.ExpandCursor) string { + if c == nil { + return "" + } + var parts []string + parts = append(parts, "kind="+string(c.Kind)) + parts = append(parts, "subject_set="+c.SubjectSet.String()) + if c.TraverseRelation != nil { + parts = append(parts, "traverseRel="+*c.TraverseRelation) + } + return "{" + strings.Join(parts, " ") + "}" +} + +// sortTree recursively sorts the children of a tree by subject string, in-place. +func sortTree(tree *relationtuple.Tree) { + if tree == nil { + return + } + for _, child := range tree.Children { + sortTree(child) + } + if tree.Children == nil { + return + } + slices.SortFunc(tree.Children, func(a, b *relationtuple.Tree) int { + if a.Subject == nil && b.Subject == nil { + return 0 + } + if a.Subject == nil { + return 1 + } + if b.Subject == nil { + return -1 + } + return strings.Compare(a.Subject.String(), b.Subject.String()) + }) +} + +// AssertInternalTreesAreEqual compares two trees for equality, ignoring child order. +func AssertInternalTreesAreEqual(t *testing.T, expected, actual *relationtuple.Tree) bool { + t.Helper() + sortTree(expected) + sortTree(actual) + return assert.Equal(t, formatTree(expected, ""), formatTree(actual, "")) +} + +// RequireInternalTreesAreEqual compares two trees for equality, ignoring child order. +func RequireInternalTreesAreEqual(t *testing.T, expected, actual *relationtuple.Tree) { + t.Helper() + sortTree(expected) + sortTree(actual) + require.Equal(t, formatTree(expected, ""), formatTree(actual, "")) +} diff --git a/internal/httpclient/.gitignore b/internal/httpclient/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/internal/httpclient/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/internal/httpclient/.openapi-generator-ignore b/internal/httpclient/.openapi-generator-ignore new file mode 100644 index 0000000..7484ee5 --- /dev/null +++ b/internal/httpclient/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/internal/httpclient/.openapi-generator/FILES b/internal/httpclient/.openapi-generator/FILES new file mode 100644 index 0000000..73bfe2d --- /dev/null +++ b/internal/httpclient/.openapi-generator/FILES @@ -0,0 +1,70 @@ +.gitignore +.openapi-generator-ignore +.travis.yml +README.md +api/openapi.yaml +api_metadata.go +api_permission.go +api_relationship.go +client.go +configuration.go +docs/BatchCheckPermissionBody.md +docs/BatchCheckPermissionResult.md +docs/CheckOplSyntaxResult.md +docs/CheckPermissionResult.md +docs/CheckPermissionResultWithError.md +docs/CreateRelationshipBody.md +docs/ErrorGeneric.md +docs/ExpandedPermissionTree.md +docs/GenericError.md +docs/GetVersion200Response.md +docs/HealthNotReadyStatus.md +docs/HealthStatus.md +docs/IsAlive200Response.md +docs/IsReady503Response.md +docs/MetadataApi.md +docs/Namespace.md +docs/ParseError.md +docs/PermissionApi.md +docs/PostCheckPermissionBody.md +docs/PostCheckPermissionOrErrorBody.md +docs/RelationQuery.md +docs/Relationship.md +docs/RelationshipApi.md +docs/RelationshipNamespaces.md +docs/RelationshipPatch.md +docs/Relationships.md +docs/SourcePosition.md +docs/SubjectSet.md +docs/Version.md +git_push.sh +go.mod +go.sum +model_batch_check_permission_body.go +model_batch_check_permission_result.go +model_check_opl_syntax_result.go +model_check_permission_result.go +model_check_permission_result_with_error.go +model_create_relationship_body.go +model_error_generic.go +model_expanded_permission_tree.go +model_generic_error.go +model_get_version_200_response.go +model_health_not_ready_status.go +model_health_status.go +model_is_alive_200_response.go +model_is_ready_503_response.go +model_namespace.go +model_parse_error.go +model_post_check_permission_body.go +model_post_check_permission_or_error_body.go +model_relation_query.go +model_relationship.go +model_relationship_namespaces.go +model_relationship_patch.go +model_relationships.go +model_source_position.go +model_subject_set.go +model_version.go +response.go +utils.go diff --git a/internal/httpclient/.openapi-generator/VERSION b/internal/httpclient/.openapi-generator/VERSION new file mode 100644 index 0000000..4b49d9b --- /dev/null +++ b/internal/httpclient/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.2.0 \ No newline at end of file diff --git a/internal/httpclient/.travis.yml b/internal/httpclient/.travis.yml new file mode 100644 index 0000000..755978d --- /dev/null +++ b/internal/httpclient/.travis.yml @@ -0,0 +1,7 @@ +language: go + +install: + - go get -d -v . + +script: + - go build -v ./ diff --git a/internal/httpclient/README.md b/internal/httpclient/README.md new file mode 100644 index 0000000..322a9e0 --- /dev/null +++ b/internal/httpclient/README.md @@ -0,0 +1,153 @@ +# Go API client for client + +Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + + +## Overview +This API client was generated by the [OpenAPI Generator](https://openapi-generator.tech) project. By using the [OpenAPI-spec](https://www.openapis.org/) from a remote server, you can easily generate an API client. + +- API version: +- Package version: 1.0.0 +- Build package: org.openapitools.codegen.languages.GoClientCodegen + +## Installation + +Install the following dependencies: + +```shell +go get github.com/stretchr/testify/assert +go get golang.org/x/oauth2 +go get golang.org/x/net/context +``` + +Put the package under your project folder and add the following in import: + +```golang +import client "github.com/ory/keto-client-go" +``` + +To use a proxy, set the environment variable `HTTP_PROXY`: + +```golang +os.Setenv("HTTP_PROXY", "http://proxy_name:proxy_port") +``` + +## Configuration of Server URL + +Default configuration comes with `Servers` field that contains server objects as defined in the OpenAPI specification. + +### Select Server Configuration + +For using other server than the one defined on index 0 set context value `sw.ContextServerIndex` of type `int`. + +```golang +ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) +``` + +### Templated Server URL + +Templated server URL is formatted using default variables from configuration or from context value `sw.ContextServerVariables` of type `map[string]string`. + +```golang +ctx := context.WithValue(context.Background(), client.ContextServerVariables, map[string]string{ + "basePath": "v2", +}) +``` + +Note, enum values are always validated and all unused variables are silently ignored. + +### URLs Configuration per Operation + +Each operation can use different server URL defined using `OperationServers` map in the `Configuration`. +An operation is uniquely identifield by `"{classname}Service.{nickname}"` string. +Similar rules for overriding default operation server index and variables applies by using `sw.ContextOperationServerIndices` and `sw.ContextOperationServerVariables` context maps. + +``` +ctx := context.WithValue(context.Background(), client.ContextOperationServerIndices, map[string]int{ + "{classname}Service.{nickname}": 2, +}) +ctx = context.WithValue(context.Background(), client.ContextOperationServerVariables, map[string]map[string]string{ + "{classname}Service.{nickname}": { + "port": "8443", + }, +}) +``` + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- +*MetadataApi* | [**GetVersion**](docs/MetadataApi.md#getversion) | **Get** /version | Return Running Software Version. +*MetadataApi* | [**IsAlive**](docs/MetadataApi.md#isalive) | **Get** /health/alive | Check HTTP Server Status +*MetadataApi* | [**IsReady**](docs/MetadataApi.md#isready) | **Get** /health/ready | Check HTTP Server and Database Status +*PermissionApi* | [**BatchCheckPermission**](docs/PermissionApi.md#batchcheckpermission) | **Post** /relation-tuples/batch/check | Batch check permissions +*PermissionApi* | [**CheckPermission**](docs/PermissionApi.md#checkpermission) | **Get** /relation-tuples/check/openapi | Check a permission +*PermissionApi* | [**CheckPermissionOrError**](docs/PermissionApi.md#checkpermissionorerror) | **Get** /relation-tuples/check | Check a permission +*PermissionApi* | [**ExpandPermissions**](docs/PermissionApi.md#expandpermissions) | **Get** /relation-tuples/expand | Expand a Relationship into permissions. +*PermissionApi* | [**PostCheckPermission**](docs/PermissionApi.md#postcheckpermission) | **Post** /relation-tuples/check/openapi | Check a permission +*PermissionApi* | [**PostCheckPermissionOrError**](docs/PermissionApi.md#postcheckpermissionorerror) | **Post** /relation-tuples/check | Check a permission +*RelationshipApi* | [**CheckOplSyntax**](docs/RelationshipApi.md#checkoplsyntax) | **Post** /opl/syntax/check | Check the syntax of an OPL file +*RelationshipApi* | [**CreateRelationship**](docs/RelationshipApi.md#createrelationship) | **Put** /admin/relation-tuples | Create a Relationship +*RelationshipApi* | [**DeleteRelationships**](docs/RelationshipApi.md#deleterelationships) | **Delete** /admin/relation-tuples | Delete Relationships +*RelationshipApi* | [**GetRelationships**](docs/RelationshipApi.md#getrelationships) | **Get** /relation-tuples | Query relationships +*RelationshipApi* | [**ListRelationshipNamespaces**](docs/RelationshipApi.md#listrelationshipnamespaces) | **Get** /namespaces | Query namespaces +*RelationshipApi* | [**PatchRelationships**](docs/RelationshipApi.md#patchrelationships) | **Patch** /admin/relation-tuples | Patch Multiple Relationships + + +## Documentation For Models + + - [BatchCheckPermissionBody](docs/BatchCheckPermissionBody.md) + - [BatchCheckPermissionResult](docs/BatchCheckPermissionResult.md) + - [CheckOplSyntaxResult](docs/CheckOplSyntaxResult.md) + - [CheckPermissionResult](docs/CheckPermissionResult.md) + - [CheckPermissionResultWithError](docs/CheckPermissionResultWithError.md) + - [CreateRelationshipBody](docs/CreateRelationshipBody.md) + - [ErrorGeneric](docs/ErrorGeneric.md) + - [ExpandedPermissionTree](docs/ExpandedPermissionTree.md) + - [GenericError](docs/GenericError.md) + - [GetVersion200Response](docs/GetVersion200Response.md) + - [HealthNotReadyStatus](docs/HealthNotReadyStatus.md) + - [HealthStatus](docs/HealthStatus.md) + - [IsAlive200Response](docs/IsAlive200Response.md) + - [IsReady503Response](docs/IsReady503Response.md) + - [Namespace](docs/Namespace.md) + - [ParseError](docs/ParseError.md) + - [PostCheckPermissionBody](docs/PostCheckPermissionBody.md) + - [PostCheckPermissionOrErrorBody](docs/PostCheckPermissionOrErrorBody.md) + - [RelationQuery](docs/RelationQuery.md) + - [Relationship](docs/Relationship.md) + - [RelationshipNamespaces](docs/RelationshipNamespaces.md) + - [RelationshipPatch](docs/RelationshipPatch.md) + - [Relationships](docs/Relationships.md) + - [SourcePosition](docs/SourcePosition.md) + - [SubjectSet](docs/SubjectSet.md) + - [Version](docs/Version.md) + + +## Documentation For Authorization + + Endpoints do not require authorization. + + +## Documentation for Utility Methods + +Due to the fact that model structure members are all pointers, this package contains +a number of utility functions to easily obtain pointers to values of basic types. +Each of these functions takes a value of the given basic type and returns a pointer to it: + +* `PtrBool` +* `PtrInt` +* `PtrInt32` +* `PtrInt64` +* `PtrFloat` +* `PtrFloat32` +* `PtrFloat64` +* `PtrString` +* `PtrTime` + +## Author + +hi@ory.sh + diff --git a/internal/httpclient/api/openapi.yaml b/internal/httpclient/api/openapi.yaml new file mode 100644 index 0000000..29a12b5 --- /dev/null +++ b/internal/httpclient/api/openapi.yaml @@ -0,0 +1,1315 @@ +openapi: 3.0.3 +info: + contact: + email: hi@ory.sh + description: | + Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + license: + name: Apache 2.0 + title: Ory Keto API + version: "" +servers: +- url: / +paths: + /admin/relation-tuples: + delete: + description: Use this endpoint to delete relationships + operationId: deleteRelationships + parameters: + - description: Namespace of the Relationship + explode: true + in: query + name: namespace + required: false + schema: + type: string + style: form + - description: Object of the Relationship + explode: true + in: query + name: object + required: false + schema: + type: string + style: form + - description: Relation of the Relationship + explode: true + in: query + name: relation + required: false + schema: + type: string + style: form + - description: SubjectID of the Relationship + explode: true + in: query + name: subject_id + required: false + schema: + type: string + style: form + - description: Namespace of the Subject Set + explode: true + in: query + name: subject_set.namespace + required: false + schema: + type: string + style: form + - description: Object of the Subject Set + explode: true + in: query + name: subject_set.object + required: false + schema: + type: string + style: form + - description: Relation of the Subject Set + explode: true + in: query + name: subject_set.relation + required: false + schema: + type: string + style: form + responses: + "204": + description: "Empty responses are sent when, for example, resources are\ + \ deleted. The HTTP status code for empty responses is typically 204." + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Delete Relationships + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-low + patch: + description: Use this endpoint to patch one or more relationships. + operationId: patchRelationships + requestBody: + content: + application/json: + schema: + items: + $ref: '#/components/schemas/relationshipPatch' + type: array + x-originalParamName: Body + responses: + "204": + description: "Empty responses are sent when, for example, resources are\ + \ deleted. The HTTP status code for empty responses is typically 204." + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Patch Multiple Relationships + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-low + put: + description: Use this endpoint to create a relationship. + operationId: createRelationship + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/createRelationshipBody' + x-originalParamName: Body + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/relationship' + description: relationship + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Create a Relationship + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-low + /health/alive: + get: + description: |- + This endpoint returns a HTTP 200 status code when Ory Keto is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the health status will never + refer to the cluster state, only to a single instance. + operationId: isAlive + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/isAlive_200_response' + description: Ory Keto is ready to accept connections. + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server Status + tags: + - metadata + /health/ready: + get: + description: |- + This endpoint returns a HTTP 200 status code when Ory Keto is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of Ory Keto, the health status will never + refer to the cluster state, only to a single instance. + operationId: isReady + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/isAlive_200_response' + description: Ory Keto is ready to accept requests. + "503": + content: + application/json: + schema: + $ref: '#/components/schemas/isReady_503_response' + description: Ory Kratos is not yet ready to accept requests. + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server and Database Status + tags: + - metadata + /namespaces: + get: + description: Get all namespaces + operationId: listRelationshipNamespaces + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/relationshipNamespaces' + description: relationshipNamespaces + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Query namespaces + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-low + /opl/syntax/check: + post: + description: The OPL file is expected in the body of the request. + operationId: checkOplSyntax + requestBody: + content: + text/plain: + schema: + $ref: '#/components/schemas/checkOplSyntaxBody' + x-originalParamName: Body + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/checkOplSyntaxResult' + description: checkOplSyntaxResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Check the syntax of an OPL file + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-medium + /relation-tuples: + get: + description: Get all relationships that match the query. Only the namespace + field is required. + operationId: getRelationships + parameters: + - description: |- + Items per Page + + This is the number of items per page to return. + For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + explode: true + in: query + name: page_size + required: false + schema: + default: 250 + format: int64 + maximum: 1000 + minimum: 1 + type: integer + style: form + - description: |- + Next Page Token + + The next page token. + For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + explode: true + in: query + name: page_token + required: false + schema: + type: string + style: form + - description: Namespace of the Relationship + explode: true + in: query + name: namespace + required: false + schema: + type: string + style: form + - description: Object of the Relationship + explode: true + in: query + name: object + required: false + schema: + type: string + style: form + - description: Relation of the Relationship + explode: true + in: query + name: relation + required: false + schema: + type: string + style: form + - description: SubjectID of the Relationship + explode: true + in: query + name: subject_id + required: false + schema: + type: string + style: form + - description: Namespace of the Subject Set + explode: true + in: query + name: subject_set.namespace + required: false + schema: + type: string + style: form + - description: Object of the Subject Set + explode: true + in: query + name: subject_set.object + required: false + schema: + type: string + style: form + - description: Relation of the Subject Set + explode: true + in: query + name: subject_set.relation + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/relationships' + description: relationships + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Query relationships + tags: + - relationship + x-ory-ratelimit-bucket: keto-admin-medium + /relation-tuples/batch/check: + post: + description: "To learn how relationship tuples and the check works, head over\ + \ to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview)." + operationId: batchCheckPermission + parameters: + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/batchCheckPermissionBody' + x-originalParamName: Body + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/batchCheckPermissionResult' + description: batchCheckPermissionResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Batch check permissions + tags: + - permission + x-ory-ratelimit-bucket: keto-public-high + /relation-tuples/check: + get: + description: "To learn how relationship tuples and the check works, head over\ + \ to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview)." + operationId: checkPermissionOrError + parameters: + - description: Namespace of the Relationship + explode: true + in: query + name: namespace + required: false + schema: + type: string + style: form + - description: Object of the Relationship + explode: true + in: query + name: object + required: false + schema: + type: string + style: form + - description: Relation of the Relationship + explode: true + in: query + name: relation + required: false + schema: + type: string + style: form + - description: SubjectID of the Relationship + explode: true + in: query + name: subject_id + required: false + schema: + type: string + style: form + - description: Namespace of the Subject Set + explode: true + in: query + name: subject_set.namespace + required: false + schema: + type: string + style: form + - description: Object of the Subject Set + explode: true + in: query + name: subject_set.object + required: false + schema: + type: string + style: form + - description: Relation of the Subject Set + explode: true + in: query + name: subject_set.relation + required: false + schema: + type: string + style: form + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Check a permission + tags: + - permission + x-ory-ratelimit-bucket: keto-public-high + post: + description: "To learn how relationship tuples and the check works, head over\ + \ to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview)." + operationId: postCheckPermissionOrError + parameters: + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/postCheckPermissionOrErrorBody' + x-originalParamName: Body + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + "403": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Check a permission + tags: + - permission + x-ory-ratelimit-bucket: keto-public-high + /relation-tuples/check/openapi: + get: + description: "To learn how relationship tuples and the check works, head over\ + \ to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview)." + operationId: checkPermission + parameters: + - description: Namespace of the Relationship + explode: true + in: query + name: namespace + required: false + schema: + type: string + style: form + - description: Object of the Relationship + explode: true + in: query + name: object + required: false + schema: + type: string + style: form + - description: Relation of the Relationship + explode: true + in: query + name: relation + required: false + schema: + type: string + style: form + - description: SubjectID of the Relationship + explode: true + in: query + name: subject_id + required: false + schema: + type: string + style: form + - description: Namespace of the Subject Set + explode: true + in: query + name: subject_set.namespace + required: false + schema: + type: string + style: form + - description: Object of the Subject Set + explode: true + in: query + name: subject_set.object + required: false + schema: + type: string + style: form + - description: Relation of the Subject Set + explode: true + in: query + name: subject_set.relation + required: false + schema: + type: string + style: form + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Check a permission + tags: + - permission + x-ory-ratelimit-bucket: keto-public-high + post: + description: "To learn how relationship tuples and the check works, head over\ + \ to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview)." + operationId: postCheckPermission + parameters: + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/postCheckPermissionBody' + x-originalParamName: Payload + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/checkPermissionResult' + description: checkPermissionResult + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Check a permission + tags: + - permission + x-ory-ratelimit-bucket: keto-public-high + /relation-tuples/expand: + get: + description: Use this endpoint to expand a relationship tuple into permissions. + operationId: expandPermissions + parameters: + - description: Namespace of the Subject Set + explode: true + in: query + name: namespace + required: true + schema: + type: string + style: form + - description: Object of the Subject Set + explode: true + in: query + name: object + required: true + schema: + type: string + style: form + - description: Relation of the Subject Set + explode: true + in: query + name: relation + required: true + schema: + type: string + style: form + - explode: true + in: query + name: max-depth + required: false + schema: + format: int64 + type: integer + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/expandedPermissionTree' + description: expandedPermissionTree + "400": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + "404": + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + default: + content: + application/json: + schema: + $ref: '#/components/schemas/errorGeneric' + description: errorGeneric + summary: Expand a Relationship into permissions. + tags: + - permission + x-ory-ratelimit-bucket: keto-admin-medium + /version: + get: + description: |- + This endpoint returns the version of Ory Keto. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the version will never + refer to the cluster state, only to a single instance. + operationId: getVersion + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/getVersion_200_response' + description: Returns the Ory Keto version. + summary: Return Running Software Version. + tags: + - metadata +components: + responses: + emptyResponse: + description: "Empty responses are sent when, for example, resources are deleted.\ + \ The HTTP status code for empty responses is typically 204." + schemas: + DefaultError: + description: |- + From https://go.dev/wiki/CodeReviewComments#receiver-type: + > Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer. + > If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating, + > prefer a pointer receiver, as it will make the intention clearer to the reader. + > Don’t mix receiver types. Choose either pointers or struct types for all available methods. + title: |- + DefaultError is not safe to shallow copy because it contains + fields which are not value types, e.g. maps. + A shallow copy would inadvertently share these underlying fields, + leading to incorrect behavior and data races in a concurrent context. + Consequently, all methods take the type by pointer and not by value. + ParseError: + example: + start: + Line: 0 + column: 6 + end: + Line: 0 + column: 6 + message: message + properties: + end: + $ref: '#/components/schemas/SourcePosition' + message: + type: string + start: + $ref: '#/components/schemas/SourcePosition' + type: object + SourcePosition: + example: + Line: 0 + column: 6 + properties: + Line: + format: int64 + type: integer + column: + format: int64 + type: integer + type: object + UUID: + format: uuid4 + type: string + batchCheckPermissionBody: + description: Batch Check Permission Body + properties: + tuples: + items: + $ref: '#/components/schemas/relationship' + type: array + type: object + batchCheckPermissionResult: + description: Batch Check Permission Result + example: + results: + - allowed: true + error: error + - allowed: true + error: error + properties: + results: + description: An array of check results. The order aligns with the input + order. + items: + $ref: '#/components/schemas/checkPermissionResultWithError' + type: array + required: + - results + type: object + checkOplSyntaxBody: + description: Ory Permission Language Document + type: string + checkOplSyntaxResult: + example: + errors: + - start: + Line: 0 + column: 6 + end: + Line: 0 + column: 6 + message: message + - start: + Line: 0 + column: 6 + end: + Line: 0 + column: 6 + message: message + properties: + errors: + description: The list of syntax errors + items: + $ref: '#/components/schemas/ParseError' + type: array + title: CheckOPLSyntaxResponse represents the response for an OPL syntax check + request. + type: object + checkPermissionResult: + description: The content of the allowed field is mirrored in the HTTP status + code. + example: + allowed: true + properties: + allowed: + description: whether the relation tuple is allowed + type: boolean + required: + - allowed + title: Check Permission Result + type: object + checkPermissionResultWithError: + description: Check Permission Result With Error + example: + allowed: true + error: error + properties: + allowed: + description: whether the relation tuple is allowed + type: boolean + error: + description: any error generated while checking the relation tuple + type: string + required: + - allowed + type: object + createRelationshipBody: + description: Create Relationship Request Body + properties: + namespace: + description: Namespace to query + type: string + object: + description: Object to query + type: string + relation: + description: Relation to query + type: string + subject_id: + description: |- + SubjectID to query + + Either SubjectSet or SubjectID can be provided. + type: string + subject_set: + $ref: '#/components/schemas/subjectSet' + type: object + errorGeneric: + description: The standard Ory JSON API error format. + properties: + error: + $ref: '#/components/schemas/genericError' + required: + - error + title: JSON API Error Response + type: object + expandedPermissionTree: + example: + tuple: + subject_id: subject_id + namespace: namespace + object: object + relation: relation + subject_set: + namespace: namespace + object: object + relation: relation + children: + - null + - null + type: union + properties: + children: + description: "The children of the node, possibly none." + items: + $ref: '#/components/schemas/expandedPermissionTree' + type: array + tuple: + $ref: '#/components/schemas/relationship' + type: + description: |- + The type of the node. + union TreeNodeUnion + exclusion TreeNodeExclusion + intersection TreeNodeIntersection + leaf TreeNodeLeaf + tuple_to_subject_set TreeNodeTupleToSubjectSet + computed_subject_set TreeNodeComputedSubjectSet + not TreeNodeNot + unspecified TreeNodeUnspecified + enum: + - union + - exclusion + - intersection + - leaf + - tuple_to_subject_set + - computed_subject_set + - not + - unspecified + type: string + x-go-enum-desc: |- + union TreeNodeUnion + exclusion TreeNodeExclusion + intersection TreeNodeIntersection + leaf TreeNodeLeaf + tuple_to_subject_set TreeNodeTupleToSubjectSet + computed_subject_set TreeNodeComputedSubjectSet + not TreeNodeNot + unspecified TreeNodeUnspecified + required: + - type + type: object + genericError: + properties: + code: + description: The status code + example: 404 + format: int64 + type: integer + debug: + description: |- + Debug information + + This field is often not exposed to protect against leaking + sensitive information. + example: SQL field "foo" is not a bool. + type: string + details: + additionalProperties: {} + description: Further error details + type: object + id: + description: |- + The error ID + + Useful when trying to identify various errors in application logic. + type: string + message: + description: |- + Error message + + The error's message. + example: The resource could not be found + type: string + reason: + description: A human-readable reason for the error + example: User with ID 1234 does not exist. + type: string + request: + description: |- + The request ID + + The request ID is often exposed internally in order to trace + errors across service architectures. This is often a UUID. + example: d7ef54b1-ec15-46e6-bccb-524b82c035e6 + type: string + status: + description: The status description + example: Not Found + type: string + required: + - message + type: object + healthNotReadyStatus: + properties: + errors: + additionalProperties: + type: string + description: Errors contains a list of errors that caused the not ready + status. + type: object + title: The not ready status of the service. + type: object + healthStatus: + properties: + status: + description: Status always contains "ok". + type: string + title: The health status of the service. + type: object + namespace: + example: + name: name + properties: + name: + description: Name of the namespace. + type: string + type: object + postCheckPermissionBody: + description: Check Permission using Post Request Body + properties: + namespace: + description: Namespace to query + type: string + object: + description: Object to query + type: string + relation: + description: Relation to query + type: string + subject_id: + description: |- + SubjectID to query + + Either SubjectSet or SubjectID can be provided. + type: string + subject_set: + $ref: '#/components/schemas/subjectSet' + type: object + postCheckPermissionOrErrorBody: + description: Post Check Permission Or Error Body + properties: + namespace: + description: Namespace to query + type: string + object: + description: Object to query + type: string + relation: + description: Relation to query + type: string + subject_id: + description: |- + SubjectID to query + + Either SubjectSet or SubjectID can be provided. + type: string + subject_set: + $ref: '#/components/schemas/subjectSet' + type: object + relationQuery: + description: Relation Query + properties: + namespace: + description: Namespace to query + type: string + object: + description: Object to query + type: string + relation: + description: Relation to query + type: string + subject_id: + description: |- + SubjectID to query + + Either SubjectSet or SubjectID can be provided. + type: string + subject_set: + $ref: '#/components/schemas/subjectSet' + type: object + relationship: + description: Relationship + example: + subject_id: subject_id + namespace: namespace + object: object + relation: relation + subject_set: + namespace: namespace + object: object + relation: relation + properties: + namespace: + description: Namespace of the Relation Tuple + type: string + object: + description: Object of the Relation Tuple + type: string + relation: + description: Relation of the Relation Tuple + type: string + subject_id: + description: |- + SubjectID of the Relation Tuple + + Either SubjectSet or SubjectID can be provided. + type: string + subject_set: + $ref: '#/components/schemas/subjectSet' + required: + - namespace + - object + - relation + type: object + relationshipNamespaces: + description: Relationship Namespace List + example: + namespaces: + - name: name + - name: name + properties: + namespaces: + items: + $ref: '#/components/schemas/namespace' + type: array + type: object + relationshipPatch: + description: Payload for patching a relationship + properties: + action: + enum: + - insert + - delete + type: string + x-go-enum-desc: |- + insert ActionInsert + delete ActionDelete + relation_tuple: + $ref: '#/components/schemas/relationship' + type: object + relationships: + description: Paginated Relationship List + example: + next_page_token: next_page_token + relation_tuples: + - subject_id: subject_id + namespace: namespace + object: object + relation: relation + subject_set: + namespace: namespace + object: object + relation: relation + - subject_id: subject_id + namespace: namespace + object: object + relation: relation + subject_set: + namespace: namespace + object: object + relation: relation + properties: + next_page_token: + description: |- + The opaque token to provide in a subsequent request + to get the next page. It is the empty string iff this is + the last page. + type: string + relation_tuples: + items: + $ref: '#/components/schemas/relationship' + type: array + type: object + subjectSet: + example: + namespace: namespace + object: object + relation: relation + properties: + namespace: + description: Namespace of the Subject Set + type: string + object: + description: Object of the Subject Set + type: string + relation: + description: Relation of the Subject Set + type: string + required: + - namespace + - object + - relation + type: object + unexpectedError: + type: string + version: + properties: + version: + description: Version is the service's version. + type: string + type: object + isAlive_200_response: + example: + status: status + properties: + status: + description: Always "ok". + type: string + required: + - status + type: object + isReady_503_response: + properties: + errors: + additionalProperties: + type: string + description: Errors contains a list of errors that caused the not ready + status. + type: object + required: + - errors + type: object + getVersion_200_response: + example: + version: version + properties: + version: + description: The version of Ory Keto. + type: string + required: + - version + type: object diff --git a/internal/httpclient/api_metadata.go b/internal/httpclient/api_metadata.go new file mode 100644 index 0000000..69095c9 --- /dev/null +++ b/internal/httpclient/api_metadata.go @@ -0,0 +1,442 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +// Linger please +var ( + _ context.Context +) + +type MetadataApi interface { + + /* + * GetVersion Return Running Software Version. + * This endpoint returns the version of Ory Keto. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the version will never + refer to the cluster state, only to a single instance. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return MetadataApiApiGetVersionRequest + */ + GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest + + /* + * GetVersionExecute executes the request + * @return GetVersion200Response + */ + GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) + + /* + * IsAlive Check HTTP Server Status + * This endpoint returns a HTTP 200 status code when Ory Keto is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the health status will never + refer to the cluster state, only to a single instance. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return MetadataApiApiIsAliveRequest + */ + IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest + + /* + * IsAliveExecute executes the request + * @return IsAlive200Response + */ + IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) + + /* + * IsReady Check HTTP Server and Database Status + * This endpoint returns a HTTP 200 status code when Ory Keto is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of Ory Keto, the health status will never + refer to the cluster state, only to a single instance. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return MetadataApiApiIsReadyRequest + */ + IsReady(ctx context.Context) MetadataApiApiIsReadyRequest + + /* + * IsReadyExecute executes the request + * @return IsAlive200Response + */ + IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) +} + +// MetadataApiService MetadataApi service +type MetadataApiService service + +type MetadataApiApiGetVersionRequest struct { + ctx context.Context + ApiService MetadataApi +} + +func (r MetadataApiApiGetVersionRequest) Execute() (*GetVersion200Response, *http.Response, error) { + return r.ApiService.GetVersionExecute(r) +} + +/* + - GetVersion Return Running Software Version. + - This endpoint returns the version of Ory Keto. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +Be aware that if you are running multiple nodes of this service, the version will never +refer to the cluster state, only to a single instance. + - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + - @return MetadataApiApiGetVersionRequest +*/ +func (a *MetadataApiService) GetVersion(ctx context.Context) MetadataApiApiGetVersionRequest { + return MetadataApiApiGetVersionRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return GetVersion200Response + */ +func (a *MetadataApiService) GetVersionExecute(r MetadataApiApiGetVersionRequest) (*GetVersion200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *GetVersion200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.GetVersion") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/version" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataApiApiIsAliveRequest struct { + ctx context.Context + ApiService MetadataApi +} + +func (r MetadataApiApiIsAliveRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsAliveExecute(r) +} + +/* + - IsAlive Check HTTP Server Status + - This endpoint returns a HTTP 200 status code when Ory Keto is accepting incoming + +HTTP requests. This status does currently not include checks whether the database connection is working. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +Be aware that if you are running multiple nodes of this service, the health status will never +refer to the cluster state, only to a single instance. + - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + - @return MetadataApiApiIsAliveRequest +*/ +func (a *MetadataApiService) IsAlive(ctx context.Context) MetadataApiApiIsAliveRequest { + return MetadataApiApiIsAliveRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return IsAlive200Response + */ +func (a *MetadataApiService) IsAliveExecute(r MetadataApiApiIsAliveRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsAlive") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/alive" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type MetadataApiApiIsReadyRequest struct { + ctx context.Context + ApiService MetadataApi +} + +func (r MetadataApiApiIsReadyRequest) Execute() (*IsAlive200Response, *http.Response, error) { + return r.ApiService.IsReadyExecute(r) +} + +/* + - IsReady Check HTTP Server and Database Status + - This endpoint returns a HTTP 200 status code when Ory Keto is up running and the environment dependencies (e.g. + +the database) are responsive as well. + +If the service supports TLS Edge Termination, this endpoint does not require the +`X-Forwarded-Proto` header to be set. + +Be aware that if you are running multiple nodes of Ory Keto, the health status will never +refer to the cluster state, only to a single instance. + - @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + - @return MetadataApiApiIsReadyRequest +*/ +func (a *MetadataApiService) IsReady(ctx context.Context) MetadataApiApiIsReadyRequest { + return MetadataApiApiIsReadyRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return IsAlive200Response + */ +func (a *MetadataApiService) IsReadyExecute(r MetadataApiApiIsReadyRequest) (*IsAlive200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *IsAlive200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MetadataApiService.IsReady") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/health/ready" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json", "text/plain"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 503 { + var v IsReady503Response + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v string + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/httpclient/api_permission.go b/internal/httpclient/api_permission.go new file mode 100644 index 0000000..30e47eb --- /dev/null +++ b/internal/httpclient/api_permission.go @@ -0,0 +1,1067 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +// Linger please +var ( + _ context.Context +) + +type PermissionApi interface { + + /* + * BatchCheckPermission Batch check permissions + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiBatchCheckPermissionRequest + */ + BatchCheckPermission(ctx context.Context) PermissionApiApiBatchCheckPermissionRequest + + /* + * BatchCheckPermissionExecute executes the request + * @return BatchCheckPermissionResult + */ + BatchCheckPermissionExecute(r PermissionApiApiBatchCheckPermissionRequest) (*BatchCheckPermissionResult, *http.Response, error) + + /* + * CheckPermission Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiCheckPermissionRequest + */ + CheckPermission(ctx context.Context) PermissionApiApiCheckPermissionRequest + + /* + * CheckPermissionExecute executes the request + * @return CheckPermissionResult + */ + CheckPermissionExecute(r PermissionApiApiCheckPermissionRequest) (*CheckPermissionResult, *http.Response, error) + + /* + * CheckPermissionOrError Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiCheckPermissionOrErrorRequest + */ + CheckPermissionOrError(ctx context.Context) PermissionApiApiCheckPermissionOrErrorRequest + + /* + * CheckPermissionOrErrorExecute executes the request + * @return CheckPermissionResult + */ + CheckPermissionOrErrorExecute(r PermissionApiApiCheckPermissionOrErrorRequest) (*CheckPermissionResult, *http.Response, error) + + /* + * ExpandPermissions Expand a Relationship into permissions. + * Use this endpoint to expand a relationship tuple into permissions. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiExpandPermissionsRequest + */ + ExpandPermissions(ctx context.Context) PermissionApiApiExpandPermissionsRequest + + /* + * ExpandPermissionsExecute executes the request + * @return ExpandedPermissionTree + */ + ExpandPermissionsExecute(r PermissionApiApiExpandPermissionsRequest) (*ExpandedPermissionTree, *http.Response, error) + + /* + * PostCheckPermission Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiPostCheckPermissionRequest + */ + PostCheckPermission(ctx context.Context) PermissionApiApiPostCheckPermissionRequest + + /* + * PostCheckPermissionExecute executes the request + * @return CheckPermissionResult + */ + PostCheckPermissionExecute(r PermissionApiApiPostCheckPermissionRequest) (*CheckPermissionResult, *http.Response, error) + + /* + * PostCheckPermissionOrError Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiPostCheckPermissionOrErrorRequest + */ + PostCheckPermissionOrError(ctx context.Context) PermissionApiApiPostCheckPermissionOrErrorRequest + + /* + * PostCheckPermissionOrErrorExecute executes the request + * @return CheckPermissionResult + */ + PostCheckPermissionOrErrorExecute(r PermissionApiApiPostCheckPermissionOrErrorRequest) (*CheckPermissionResult, *http.Response, error) +} + +// PermissionApiService PermissionApi service +type PermissionApiService service + +type PermissionApiApiBatchCheckPermissionRequest struct { + ctx context.Context + ApiService PermissionApi + maxDepth *int64 + batchCheckPermissionBody *BatchCheckPermissionBody +} + +func (r PermissionApiApiBatchCheckPermissionRequest) MaxDepth(maxDepth int64) PermissionApiApiBatchCheckPermissionRequest { + r.maxDepth = &maxDepth + return r +} +func (r PermissionApiApiBatchCheckPermissionRequest) BatchCheckPermissionBody(batchCheckPermissionBody BatchCheckPermissionBody) PermissionApiApiBatchCheckPermissionRequest { + r.batchCheckPermissionBody = &batchCheckPermissionBody + return r +} + +func (r PermissionApiApiBatchCheckPermissionRequest) Execute() (*BatchCheckPermissionResult, *http.Response, error) { + return r.ApiService.BatchCheckPermissionExecute(r) +} + +/* + * BatchCheckPermission Batch check permissions + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiBatchCheckPermissionRequest + */ +func (a *PermissionApiService) BatchCheckPermission(ctx context.Context) PermissionApiApiBatchCheckPermissionRequest { + return PermissionApiApiBatchCheckPermissionRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return BatchCheckPermissionResult + */ +func (a *PermissionApiService) BatchCheckPermissionExecute(r PermissionApiApiBatchCheckPermissionRequest) (*BatchCheckPermissionResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *BatchCheckPermissionResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.BatchCheckPermission") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/batch/check" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.batchCheckPermissionBody + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type PermissionApiApiCheckPermissionRequest struct { + ctx context.Context + ApiService PermissionApi + namespace *string + object *string + relation *string + subjectId *string + subjectSetNamespace *string + subjectSetObject *string + subjectSetRelation *string + maxDepth *int64 +} + +func (r PermissionApiApiCheckPermissionRequest) Namespace(namespace string) PermissionApiApiCheckPermissionRequest { + r.namespace = &namespace + return r +} +func (r PermissionApiApiCheckPermissionRequest) Object(object string) PermissionApiApiCheckPermissionRequest { + r.object = &object + return r +} +func (r PermissionApiApiCheckPermissionRequest) Relation(relation string) PermissionApiApiCheckPermissionRequest { + r.relation = &relation + return r +} +func (r PermissionApiApiCheckPermissionRequest) SubjectId(subjectId string) PermissionApiApiCheckPermissionRequest { + r.subjectId = &subjectId + return r +} +func (r PermissionApiApiCheckPermissionRequest) SubjectSetNamespace(subjectSetNamespace string) PermissionApiApiCheckPermissionRequest { + r.subjectSetNamespace = &subjectSetNamespace + return r +} +func (r PermissionApiApiCheckPermissionRequest) SubjectSetObject(subjectSetObject string) PermissionApiApiCheckPermissionRequest { + r.subjectSetObject = &subjectSetObject + return r +} +func (r PermissionApiApiCheckPermissionRequest) SubjectSetRelation(subjectSetRelation string) PermissionApiApiCheckPermissionRequest { + r.subjectSetRelation = &subjectSetRelation + return r +} +func (r PermissionApiApiCheckPermissionRequest) MaxDepth(maxDepth int64) PermissionApiApiCheckPermissionRequest { + r.maxDepth = &maxDepth + return r +} + +func (r PermissionApiApiCheckPermissionRequest) Execute() (*CheckPermissionResult, *http.Response, error) { + return r.ApiService.CheckPermissionExecute(r) +} + +/* + * CheckPermission Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiCheckPermissionRequest + */ +func (a *PermissionApiService) CheckPermission(ctx context.Context) PermissionApiApiCheckPermissionRequest { + return PermissionApiApiCheckPermissionRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return CheckPermissionResult + */ +func (a *PermissionApiService) CheckPermissionExecute(r PermissionApiApiCheckPermissionRequest) (*CheckPermissionResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *CheckPermissionResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.CheckPermission") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/check/openapi" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.namespace != nil { + localVarQueryParams.Add("namespace", parameterToString(*r.namespace, "")) + } + if r.object != nil { + localVarQueryParams.Add("object", parameterToString(*r.object, "")) + } + if r.relation != nil { + localVarQueryParams.Add("relation", parameterToString(*r.relation, "")) + } + if r.subjectId != nil { + localVarQueryParams.Add("subject_id", parameterToString(*r.subjectId, "")) + } + if r.subjectSetNamespace != nil { + localVarQueryParams.Add("subject_set.namespace", parameterToString(*r.subjectSetNamespace, "")) + } + if r.subjectSetObject != nil { + localVarQueryParams.Add("subject_set.object", parameterToString(*r.subjectSetObject, "")) + } + if r.subjectSetRelation != nil { + localVarQueryParams.Add("subject_set.relation", parameterToString(*r.subjectSetRelation, "")) + } + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type PermissionApiApiCheckPermissionOrErrorRequest struct { + ctx context.Context + ApiService PermissionApi + namespace *string + object *string + relation *string + subjectId *string + subjectSetNamespace *string + subjectSetObject *string + subjectSetRelation *string + maxDepth *int64 +} + +func (r PermissionApiApiCheckPermissionOrErrorRequest) Namespace(namespace string) PermissionApiApiCheckPermissionOrErrorRequest { + r.namespace = &namespace + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) Object(object string) PermissionApiApiCheckPermissionOrErrorRequest { + r.object = &object + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) Relation(relation string) PermissionApiApiCheckPermissionOrErrorRequest { + r.relation = &relation + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) SubjectId(subjectId string) PermissionApiApiCheckPermissionOrErrorRequest { + r.subjectId = &subjectId + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) SubjectSetNamespace(subjectSetNamespace string) PermissionApiApiCheckPermissionOrErrorRequest { + r.subjectSetNamespace = &subjectSetNamespace + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) SubjectSetObject(subjectSetObject string) PermissionApiApiCheckPermissionOrErrorRequest { + r.subjectSetObject = &subjectSetObject + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) SubjectSetRelation(subjectSetRelation string) PermissionApiApiCheckPermissionOrErrorRequest { + r.subjectSetRelation = &subjectSetRelation + return r +} +func (r PermissionApiApiCheckPermissionOrErrorRequest) MaxDepth(maxDepth int64) PermissionApiApiCheckPermissionOrErrorRequest { + r.maxDepth = &maxDepth + return r +} + +func (r PermissionApiApiCheckPermissionOrErrorRequest) Execute() (*CheckPermissionResult, *http.Response, error) { + return r.ApiService.CheckPermissionOrErrorExecute(r) +} + +/* + * CheckPermissionOrError Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiCheckPermissionOrErrorRequest + */ +func (a *PermissionApiService) CheckPermissionOrError(ctx context.Context) PermissionApiApiCheckPermissionOrErrorRequest { + return PermissionApiApiCheckPermissionOrErrorRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return CheckPermissionResult + */ +func (a *PermissionApiService) CheckPermissionOrErrorExecute(r PermissionApiApiCheckPermissionOrErrorRequest) (*CheckPermissionResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *CheckPermissionResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.CheckPermissionOrError") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/check" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.namespace != nil { + localVarQueryParams.Add("namespace", parameterToString(*r.namespace, "")) + } + if r.object != nil { + localVarQueryParams.Add("object", parameterToString(*r.object, "")) + } + if r.relation != nil { + localVarQueryParams.Add("relation", parameterToString(*r.relation, "")) + } + if r.subjectId != nil { + localVarQueryParams.Add("subject_id", parameterToString(*r.subjectId, "")) + } + if r.subjectSetNamespace != nil { + localVarQueryParams.Add("subject_set.namespace", parameterToString(*r.subjectSetNamespace, "")) + } + if r.subjectSetObject != nil { + localVarQueryParams.Add("subject_set.object", parameterToString(*r.subjectSetObject, "")) + } + if r.subjectSetRelation != nil { + localVarQueryParams.Add("subject_set.relation", parameterToString(*r.subjectSetRelation, "")) + } + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v CheckPermissionResult + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type PermissionApiApiExpandPermissionsRequest struct { + ctx context.Context + ApiService PermissionApi + namespace *string + object *string + relation *string + maxDepth *int64 +} + +func (r PermissionApiApiExpandPermissionsRequest) Namespace(namespace string) PermissionApiApiExpandPermissionsRequest { + r.namespace = &namespace + return r +} +func (r PermissionApiApiExpandPermissionsRequest) Object(object string) PermissionApiApiExpandPermissionsRequest { + r.object = &object + return r +} +func (r PermissionApiApiExpandPermissionsRequest) Relation(relation string) PermissionApiApiExpandPermissionsRequest { + r.relation = &relation + return r +} +func (r PermissionApiApiExpandPermissionsRequest) MaxDepth(maxDepth int64) PermissionApiApiExpandPermissionsRequest { + r.maxDepth = &maxDepth + return r +} + +func (r PermissionApiApiExpandPermissionsRequest) Execute() (*ExpandedPermissionTree, *http.Response, error) { + return r.ApiService.ExpandPermissionsExecute(r) +} + +/* + * ExpandPermissions Expand a Relationship into permissions. + * Use this endpoint to expand a relationship tuple into permissions. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiExpandPermissionsRequest + */ +func (a *PermissionApiService) ExpandPermissions(ctx context.Context) PermissionApiApiExpandPermissionsRequest { + return PermissionApiApiExpandPermissionsRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return ExpandedPermissionTree + */ +func (a *PermissionApiService) ExpandPermissionsExecute(r PermissionApiApiExpandPermissionsRequest) (*ExpandedPermissionTree, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *ExpandedPermissionTree + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.ExpandPermissions") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/expand" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.namespace == nil { + return localVarReturnValue, nil, reportError("namespace is required and must be specified") + } + if r.object == nil { + return localVarReturnValue, nil, reportError("object is required and must be specified") + } + if r.relation == nil { + return localVarReturnValue, nil, reportError("relation is required and must be specified") + } + + localVarQueryParams.Add("namespace", parameterToString(*r.namespace, "")) + localVarQueryParams.Add("object", parameterToString(*r.object, "")) + localVarQueryParams.Add("relation", parameterToString(*r.relation, "")) + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type PermissionApiApiPostCheckPermissionRequest struct { + ctx context.Context + ApiService PermissionApi + maxDepth *int64 + postCheckPermissionBody *PostCheckPermissionBody +} + +func (r PermissionApiApiPostCheckPermissionRequest) MaxDepth(maxDepth int64) PermissionApiApiPostCheckPermissionRequest { + r.maxDepth = &maxDepth + return r +} +func (r PermissionApiApiPostCheckPermissionRequest) PostCheckPermissionBody(postCheckPermissionBody PostCheckPermissionBody) PermissionApiApiPostCheckPermissionRequest { + r.postCheckPermissionBody = &postCheckPermissionBody + return r +} + +func (r PermissionApiApiPostCheckPermissionRequest) Execute() (*CheckPermissionResult, *http.Response, error) { + return r.ApiService.PostCheckPermissionExecute(r) +} + +/* + * PostCheckPermission Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiPostCheckPermissionRequest + */ +func (a *PermissionApiService) PostCheckPermission(ctx context.Context) PermissionApiApiPostCheckPermissionRequest { + return PermissionApiApiPostCheckPermissionRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return CheckPermissionResult + */ +func (a *PermissionApiService) PostCheckPermissionExecute(r PermissionApiApiPostCheckPermissionRequest) (*CheckPermissionResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *CheckPermissionResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.PostCheckPermission") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/check/openapi" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.postCheckPermissionBody + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type PermissionApiApiPostCheckPermissionOrErrorRequest struct { + ctx context.Context + ApiService PermissionApi + maxDepth *int64 + postCheckPermissionOrErrorBody *PostCheckPermissionOrErrorBody +} + +func (r PermissionApiApiPostCheckPermissionOrErrorRequest) MaxDepth(maxDepth int64) PermissionApiApiPostCheckPermissionOrErrorRequest { + r.maxDepth = &maxDepth + return r +} +func (r PermissionApiApiPostCheckPermissionOrErrorRequest) PostCheckPermissionOrErrorBody(postCheckPermissionOrErrorBody PostCheckPermissionOrErrorBody) PermissionApiApiPostCheckPermissionOrErrorRequest { + r.postCheckPermissionOrErrorBody = &postCheckPermissionOrErrorBody + return r +} + +func (r PermissionApiApiPostCheckPermissionOrErrorRequest) Execute() (*CheckPermissionResult, *http.Response, error) { + return r.ApiService.PostCheckPermissionOrErrorExecute(r) +} + +/* + * PostCheckPermissionOrError Check a permission + * To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview). + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return PermissionApiApiPostCheckPermissionOrErrorRequest + */ +func (a *PermissionApiService) PostCheckPermissionOrError(ctx context.Context) PermissionApiApiPostCheckPermissionOrErrorRequest { + return PermissionApiApiPostCheckPermissionOrErrorRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return CheckPermissionResult + */ +func (a *PermissionApiService) PostCheckPermissionOrErrorExecute(r PermissionApiApiPostCheckPermissionOrErrorRequest) (*CheckPermissionResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *CheckPermissionResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "PermissionApiService.PostCheckPermissionOrError") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples/check" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.maxDepth != nil { + localVarQueryParams.Add("max-depth", parameterToString(*r.maxDepth, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.postCheckPermissionOrErrorBody + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v CheckPermissionResult + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/internal/httpclient/api_relationship.go b/internal/httpclient/api_relationship.go new file mode 100644 index 0000000..46f7804 --- /dev/null +++ b/internal/httpclient/api_relationship.go @@ -0,0 +1,953 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + +// Linger please +var ( + _ context.Context +) + +type RelationshipApi interface { + + /* + * CheckOplSyntax Check the syntax of an OPL file + * The OPL file is expected in the body of the request. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiCheckOplSyntaxRequest + */ + CheckOplSyntax(ctx context.Context) RelationshipApiApiCheckOplSyntaxRequest + + /* + * CheckOplSyntaxExecute executes the request + * @return CheckOplSyntaxResult + */ + CheckOplSyntaxExecute(r RelationshipApiApiCheckOplSyntaxRequest) (*CheckOplSyntaxResult, *http.Response, error) + + /* + * CreateRelationship Create a Relationship + * Use this endpoint to create a relationship. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiCreateRelationshipRequest + */ + CreateRelationship(ctx context.Context) RelationshipApiApiCreateRelationshipRequest + + /* + * CreateRelationshipExecute executes the request + * @return Relationship + */ + CreateRelationshipExecute(r RelationshipApiApiCreateRelationshipRequest) (*Relationship, *http.Response, error) + + /* + * DeleteRelationships Delete Relationships + * Use this endpoint to delete relationships + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiDeleteRelationshipsRequest + */ + DeleteRelationships(ctx context.Context) RelationshipApiApiDeleteRelationshipsRequest + + /* + * DeleteRelationshipsExecute executes the request + */ + DeleteRelationshipsExecute(r RelationshipApiApiDeleteRelationshipsRequest) (*http.Response, error) + + /* + * GetRelationships Query relationships + * Get all relationships that match the query. Only the namespace field is required. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiGetRelationshipsRequest + */ + GetRelationships(ctx context.Context) RelationshipApiApiGetRelationshipsRequest + + /* + * GetRelationshipsExecute executes the request + * @return Relationships + */ + GetRelationshipsExecute(r RelationshipApiApiGetRelationshipsRequest) (*Relationships, *http.Response, error) + + /* + * ListRelationshipNamespaces Query namespaces + * Get all namespaces + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiListRelationshipNamespacesRequest + */ + ListRelationshipNamespaces(ctx context.Context) RelationshipApiApiListRelationshipNamespacesRequest + + /* + * ListRelationshipNamespacesExecute executes the request + * @return RelationshipNamespaces + */ + ListRelationshipNamespacesExecute(r RelationshipApiApiListRelationshipNamespacesRequest) (*RelationshipNamespaces, *http.Response, error) + + /* + * PatchRelationships Patch Multiple Relationships + * Use this endpoint to patch one or more relationships. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiPatchRelationshipsRequest + */ + PatchRelationships(ctx context.Context) RelationshipApiApiPatchRelationshipsRequest + + /* + * PatchRelationshipsExecute executes the request + */ + PatchRelationshipsExecute(r RelationshipApiApiPatchRelationshipsRequest) (*http.Response, error) +} + +// RelationshipApiService RelationshipApi service +type RelationshipApiService service + +type RelationshipApiApiCheckOplSyntaxRequest struct { + ctx context.Context + ApiService RelationshipApi + body *string +} + +func (r RelationshipApiApiCheckOplSyntaxRequest) Body(body string) RelationshipApiApiCheckOplSyntaxRequest { + r.body = &body + return r +} + +func (r RelationshipApiApiCheckOplSyntaxRequest) Execute() (*CheckOplSyntaxResult, *http.Response, error) { + return r.ApiService.CheckOplSyntaxExecute(r) +} + +/* + * CheckOplSyntax Check the syntax of an OPL file + * The OPL file is expected in the body of the request. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiCheckOplSyntaxRequest + */ +func (a *RelationshipApiService) CheckOplSyntax(ctx context.Context) RelationshipApiApiCheckOplSyntaxRequest { + return RelationshipApiApiCheckOplSyntaxRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return CheckOplSyntaxResult + */ +func (a *RelationshipApiService) CheckOplSyntaxExecute(r RelationshipApiApiCheckOplSyntaxRequest) (*CheckOplSyntaxResult, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *CheckOplSyntaxResult + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.CheckOplSyntax") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/opl/syntax/check" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"text/plain"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.body + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type RelationshipApiApiCreateRelationshipRequest struct { + ctx context.Context + ApiService RelationshipApi + createRelationshipBody *CreateRelationshipBody +} + +func (r RelationshipApiApiCreateRelationshipRequest) CreateRelationshipBody(createRelationshipBody CreateRelationshipBody) RelationshipApiApiCreateRelationshipRequest { + r.createRelationshipBody = &createRelationshipBody + return r +} + +func (r RelationshipApiApiCreateRelationshipRequest) Execute() (*Relationship, *http.Response, error) { + return r.ApiService.CreateRelationshipExecute(r) +} + +/* + * CreateRelationship Create a Relationship + * Use this endpoint to create a relationship. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiCreateRelationshipRequest + */ +func (a *RelationshipApiService) CreateRelationship(ctx context.Context) RelationshipApiApiCreateRelationshipRequest { + return RelationshipApiApiCreateRelationshipRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return Relationship + */ +func (a *RelationshipApiService) CreateRelationshipExecute(r RelationshipApiApiCreateRelationshipRequest) (*Relationship, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPut + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *Relationship + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.CreateRelationship") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/relation-tuples" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.createRelationshipBody + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type RelationshipApiApiDeleteRelationshipsRequest struct { + ctx context.Context + ApiService RelationshipApi + namespace *string + object *string + relation *string + subjectId *string + subjectSetNamespace *string + subjectSetObject *string + subjectSetRelation *string +} + +func (r RelationshipApiApiDeleteRelationshipsRequest) Namespace(namespace string) RelationshipApiApiDeleteRelationshipsRequest { + r.namespace = &namespace + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) Object(object string) RelationshipApiApiDeleteRelationshipsRequest { + r.object = &object + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) Relation(relation string) RelationshipApiApiDeleteRelationshipsRequest { + r.relation = &relation + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) SubjectId(subjectId string) RelationshipApiApiDeleteRelationshipsRequest { + r.subjectId = &subjectId + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) SubjectSetNamespace(subjectSetNamespace string) RelationshipApiApiDeleteRelationshipsRequest { + r.subjectSetNamespace = &subjectSetNamespace + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) SubjectSetObject(subjectSetObject string) RelationshipApiApiDeleteRelationshipsRequest { + r.subjectSetObject = &subjectSetObject + return r +} +func (r RelationshipApiApiDeleteRelationshipsRequest) SubjectSetRelation(subjectSetRelation string) RelationshipApiApiDeleteRelationshipsRequest { + r.subjectSetRelation = &subjectSetRelation + return r +} + +func (r RelationshipApiApiDeleteRelationshipsRequest) Execute() (*http.Response, error) { + return r.ApiService.DeleteRelationshipsExecute(r) +} + +/* + * DeleteRelationships Delete Relationships + * Use this endpoint to delete relationships + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiDeleteRelationshipsRequest + */ +func (a *RelationshipApiService) DeleteRelationships(ctx context.Context) RelationshipApiApiDeleteRelationshipsRequest { + return RelationshipApiApiDeleteRelationshipsRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + */ +func (a *RelationshipApiService) DeleteRelationshipsExecute(r RelationshipApiApiDeleteRelationshipsRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.DeleteRelationships") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/relation-tuples" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.namespace != nil { + localVarQueryParams.Add("namespace", parameterToString(*r.namespace, "")) + } + if r.object != nil { + localVarQueryParams.Add("object", parameterToString(*r.object, "")) + } + if r.relation != nil { + localVarQueryParams.Add("relation", parameterToString(*r.relation, "")) + } + if r.subjectId != nil { + localVarQueryParams.Add("subject_id", parameterToString(*r.subjectId, "")) + } + if r.subjectSetNamespace != nil { + localVarQueryParams.Add("subject_set.namespace", parameterToString(*r.subjectSetNamespace, "")) + } + if r.subjectSetObject != nil { + localVarQueryParams.Add("subject_set.object", parameterToString(*r.subjectSetObject, "")) + } + if r.subjectSetRelation != nil { + localVarQueryParams.Add("subject_set.relation", parameterToString(*r.subjectSetRelation, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} + +type RelationshipApiApiGetRelationshipsRequest struct { + ctx context.Context + ApiService RelationshipApi + pageSize *int64 + pageToken *string + namespace *string + object *string + relation *string + subjectId *string + subjectSetNamespace *string + subjectSetObject *string + subjectSetRelation *string +} + +func (r RelationshipApiApiGetRelationshipsRequest) PageSize(pageSize int64) RelationshipApiApiGetRelationshipsRequest { + r.pageSize = &pageSize + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) PageToken(pageToken string) RelationshipApiApiGetRelationshipsRequest { + r.pageToken = &pageToken + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) Namespace(namespace string) RelationshipApiApiGetRelationshipsRequest { + r.namespace = &namespace + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) Object(object string) RelationshipApiApiGetRelationshipsRequest { + r.object = &object + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) Relation(relation string) RelationshipApiApiGetRelationshipsRequest { + r.relation = &relation + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) SubjectId(subjectId string) RelationshipApiApiGetRelationshipsRequest { + r.subjectId = &subjectId + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) SubjectSetNamespace(subjectSetNamespace string) RelationshipApiApiGetRelationshipsRequest { + r.subjectSetNamespace = &subjectSetNamespace + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) SubjectSetObject(subjectSetObject string) RelationshipApiApiGetRelationshipsRequest { + r.subjectSetObject = &subjectSetObject + return r +} +func (r RelationshipApiApiGetRelationshipsRequest) SubjectSetRelation(subjectSetRelation string) RelationshipApiApiGetRelationshipsRequest { + r.subjectSetRelation = &subjectSetRelation + return r +} + +func (r RelationshipApiApiGetRelationshipsRequest) Execute() (*Relationships, *http.Response, error) { + return r.ApiService.GetRelationshipsExecute(r) +} + +/* + * GetRelationships Query relationships + * Get all relationships that match the query. Only the namespace field is required. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiGetRelationshipsRequest + */ +func (a *RelationshipApiService) GetRelationships(ctx context.Context) RelationshipApiApiGetRelationshipsRequest { + return RelationshipApiApiGetRelationshipsRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return Relationships + */ +func (a *RelationshipApiService) GetRelationshipsExecute(r RelationshipApiApiGetRelationshipsRequest) (*Relationships, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *Relationships + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.GetRelationships") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/relation-tuples" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + if r.pageSize != nil { + localVarQueryParams.Add("page_size", parameterToString(*r.pageSize, "")) + } + if r.pageToken != nil { + localVarQueryParams.Add("page_token", parameterToString(*r.pageToken, "")) + } + if r.namespace != nil { + localVarQueryParams.Add("namespace", parameterToString(*r.namespace, "")) + } + if r.object != nil { + localVarQueryParams.Add("object", parameterToString(*r.object, "")) + } + if r.relation != nil { + localVarQueryParams.Add("relation", parameterToString(*r.relation, "")) + } + if r.subjectId != nil { + localVarQueryParams.Add("subject_id", parameterToString(*r.subjectId, "")) + } + if r.subjectSetNamespace != nil { + localVarQueryParams.Add("subject_set.namespace", parameterToString(*r.subjectSetNamespace, "")) + } + if r.subjectSetObject != nil { + localVarQueryParams.Add("subject_set.object", parameterToString(*r.subjectSetObject, "")) + } + if r.subjectSetRelation != nil { + localVarQueryParams.Add("subject_set.relation", parameterToString(*r.subjectSetRelation, "")) + } + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type RelationshipApiApiListRelationshipNamespacesRequest struct { + ctx context.Context + ApiService RelationshipApi +} + +func (r RelationshipApiApiListRelationshipNamespacesRequest) Execute() (*RelationshipNamespaces, *http.Response, error) { + return r.ApiService.ListRelationshipNamespacesExecute(r) +} + +/* + * ListRelationshipNamespaces Query namespaces + * Get all namespaces + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiListRelationshipNamespacesRequest + */ +func (a *RelationshipApiService) ListRelationshipNamespaces(ctx context.Context) RelationshipApiApiListRelationshipNamespacesRequest { + return RelationshipApiApiListRelationshipNamespacesRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + * @return RelationshipNamespaces + */ +func (a *RelationshipApiService) ListRelationshipNamespacesExecute(r RelationshipApiApiListRelationshipNamespacesRequest) (*RelationshipNamespaces, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + localVarReturnValue *RelationshipNamespaces + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.ListRelationshipNamespaces") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/namespaces" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type RelationshipApiApiPatchRelationshipsRequest struct { + ctx context.Context + ApiService RelationshipApi + relationshipPatch *[]RelationshipPatch +} + +func (r RelationshipApiApiPatchRelationshipsRequest) RelationshipPatch(relationshipPatch []RelationshipPatch) RelationshipApiApiPatchRelationshipsRequest { + r.relationshipPatch = &relationshipPatch + return r +} + +func (r RelationshipApiApiPatchRelationshipsRequest) Execute() (*http.Response, error) { + return r.ApiService.PatchRelationshipsExecute(r) +} + +/* + * PatchRelationships Patch Multiple Relationships + * Use this endpoint to patch one or more relationships. + * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + * @return RelationshipApiApiPatchRelationshipsRequest + */ +func (a *RelationshipApiService) PatchRelationships(ctx context.Context) RelationshipApiApiPatchRelationshipsRequest { + return RelationshipApiApiPatchRelationshipsRequest{ + ApiService: a, + ctx: ctx, + } +} + +/* + * Execute executes the request + */ +func (a *RelationshipApiService) PatchRelationshipsExecute(r RelationshipApiApiPatchRelationshipsRequest) (*http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + localVarFormFileName string + localVarFileName string + localVarFileBytes []byte + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "RelationshipApiService.PatchRelationships") + if err != nil { + return nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/admin/relation-tuples" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.relationshipPatch + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes) + if err != nil { + return nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + var v ErrorGeneric + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.model = v + return localVarHTTPResponse, newErr + } + + return localVarHTTPResponse, nil +} diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go new file mode 100644 index 0000000..3a1749f --- /dev/null +++ b/internal/httpclient/client.go @@ -0,0 +1,547 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "bytes" + "context" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io" + "log" + "mime/multipart" + "net/http" + "net/http/httputil" + "net/url" + "os" + "path/filepath" + "reflect" + "regexp" + "strconv" + "strings" + "time" + "unicode/utf8" + + "golang.org/x/oauth2" +) + +var ( + jsonCheck = regexp.MustCompile(`(?i:(?:application|text)/(?:vnd\.[^;]+\+)?json)`) + xmlCheck = regexp.MustCompile(`(?i:(?:application|text)/xml)`) +) + +// APIClient manages communication with the Ory Keto API API v +// In most cases there should be only one, shared, APIClient. +type APIClient struct { + cfg *Configuration + common service // Reuse a single struct instead of allocating one for each service on the heap. + + // API Services + + MetadataApi MetadataApi + + PermissionApi PermissionApi + + RelationshipApi RelationshipApi +} + +type service struct { + client *APIClient +} + +// NewAPIClient creates a new API client. Requires a userAgent string describing your application. +// optionally a custom http.Client to allow for advanced features such as caching. +func NewAPIClient(cfg *Configuration) *APIClient { + if cfg.HTTPClient == nil { + cfg.HTTPClient = http.DefaultClient + } + + c := &APIClient{} + c.cfg = cfg + c.common.client = c + + // API Services + c.MetadataApi = (*MetadataApiService)(&c.common) + c.PermissionApi = (*PermissionApiService)(&c.common) + c.RelationshipApi = (*RelationshipApiService)(&c.common) + + return c +} + +func atoi(in string) (int, error) { + return strconv.Atoi(in) +} + +// selectHeaderContentType select a content type from the available list. +func selectHeaderContentType(contentTypes []string) string { + if len(contentTypes) == 0 { + return "" + } + if contains(contentTypes, "application/json") { + return "application/json" + } + return contentTypes[0] // use the first content type specified in 'consumes' +} + +// selectHeaderAccept join all accept types and return +func selectHeaderAccept(accepts []string) string { + if len(accepts) == 0 { + return "" + } + + if contains(accepts, "application/json") { + return "application/json" + } + + return strings.Join(accepts, ",") +} + +// contains is a case insenstive match, finding needle in a haystack +func contains(haystack []string, needle string) bool { + for _, a := range haystack { + if strings.ToLower(a) == strings.ToLower(needle) { + return true + } + } + return false +} + +// Verify optional parameters are of the correct type. +func typeCheckParameter(obj interface{}, expected string, name string) error { + // Make sure there is an object. + if obj == nil { + return nil + } + + // Check the type is as expected. + if reflect.TypeOf(obj).String() != expected { + return fmt.Errorf("Expected %s to be of type %s but received %s.", name, expected, reflect.TypeOf(obj).String()) + } + return nil +} + +// parameterToString convert interface{} parameters to string, using a delimiter if format is provided. +func parameterToString(obj interface{}, collectionFormat string) string { + var delimiter string + + switch collectionFormat { + case "pipes": + delimiter = "|" + case "ssv": + delimiter = " " + case "tsv": + delimiter = "\t" + case "csv": + delimiter = "," + } + + if reflect.TypeOf(obj).Kind() == reflect.Slice { + return strings.Trim(strings.Replace(fmt.Sprint(obj), " ", delimiter, -1), "[]") + } else if t, ok := obj.(time.Time); ok { + return t.Format(time.RFC3339) + } + + return fmt.Sprintf("%v", obj) +} + +// helper for converting interface{} parameters to json strings +func parameterToJson(obj interface{}) (string, error) { + jsonBuf, err := json.Marshal(obj) + if err != nil { + return "", err + } + return string(jsonBuf), err +} + +// callAPI do the request. +func (c *APIClient) callAPI(request *http.Request) (*http.Response, error) { + if c.cfg.Debug { + dump, err := httputil.DumpRequestOut(request, true) + if err != nil { + return nil, err + } + log.Printf("\n%s\n", string(dump)) + } + + resp, err := c.cfg.HTTPClient.Do(request) + if err != nil { + return resp, err + } + + if c.cfg.Debug { + dump, err := httputil.DumpResponse(resp, true) + if err != nil { + return resp, err + } + log.Printf("\n%s\n", string(dump)) + } + return resp, err +} + +// Allow modification of underlying config for alternate implementations and testing +// Caution: modifying the configuration while live can cause data races and potentially unwanted behavior +func (c *APIClient) GetConfig() *Configuration { + return c.cfg +} + +// prepareRequest build the request +func (c *APIClient) prepareRequest( + ctx context.Context, + path string, method string, + postBody interface{}, + headerParams map[string]string, + queryParams url.Values, + formParams url.Values, + formFileName string, + fileName string, + fileBytes []byte) (localVarRequest *http.Request, err error) { + + var body *bytes.Buffer + + // Detect postBody type and post. + if postBody != nil { + contentType := headerParams["Content-Type"] + if contentType == "" { + contentType = detectContentType(postBody) + headerParams["Content-Type"] = contentType + } + + body, err = setBody(postBody, contentType) + if err != nil { + return nil, err + } + } + + // add form parameters and file if available. + if strings.HasPrefix(headerParams["Content-Type"], "multipart/form-data") && len(formParams) > 0 || (len(fileBytes) > 0 && fileName != "") { + if body != nil { + return nil, errors.New("Cannot specify postBody and multipart form at the same time.") + } + body = &bytes.Buffer{} + w := multipart.NewWriter(body) + + for k, v := range formParams { + for _, iv := range v { + if strings.HasPrefix(k, "@") { // file + err = addFile(w, k[1:], iv) + if err != nil { + return nil, err + } + } else { // form value + w.WriteField(k, iv) + } + } + } + if len(fileBytes) > 0 && fileName != "" { + w.Boundary() + //_, fileNm := filepath.Split(fileName) + part, err := w.CreateFormFile(formFileName, filepath.Base(fileName)) + if err != nil { + return nil, err + } + _, err = part.Write(fileBytes) + if err != nil { + return nil, err + } + } + + // Set the Boundary in the Content-Type + headerParams["Content-Type"] = w.FormDataContentType() + + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + w.Close() + } + + if strings.HasPrefix(headerParams["Content-Type"], "application/x-www-form-urlencoded") && len(formParams) > 0 { + if body != nil { + return nil, errors.New("Cannot specify postBody and x-www-form-urlencoded form at the same time.") + } + body = &bytes.Buffer{} + body.WriteString(formParams.Encode()) + // Set Content-Length + headerParams["Content-Length"] = fmt.Sprintf("%d", body.Len()) + } + + // Setup path and query parameters + url, err := url.Parse(path) + if err != nil { + return nil, err + } + + // Override request host, if applicable + if c.cfg.Host != "" { + url.Host = c.cfg.Host + } + + // Override request scheme, if applicable + if c.cfg.Scheme != "" { + url.Scheme = c.cfg.Scheme + } + + // Adding Query Param + query := url.Query() + for k, v := range queryParams { + for _, iv := range v { + query.Add(k, iv) + } + } + + // Encode the parameters. + url.RawQuery = query.Encode() + + // Generate a new request + if body != nil { + localVarRequest, err = http.NewRequest(method, url.String(), body) + } else { + localVarRequest, err = http.NewRequest(method, url.String(), nil) + } + if err != nil { + return nil, err + } + + // add header parameters, if any + if len(headerParams) > 0 { + headers := http.Header{} + for h, v := range headerParams { + headers.Set(h, v) + } + localVarRequest.Header = headers + } + + // Add the user agent to the request. + localVarRequest.Header.Add("User-Agent", c.cfg.UserAgent) + + if ctx != nil { + // add context to the request + localVarRequest = localVarRequest.WithContext(ctx) + + // Walk through any authentication. + + // OAuth2 authentication + if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok { + // We were able to grab an oauth2 token from the context + var latestToken *oauth2.Token + if latestToken, err = tok.Token(); err != nil { + return nil, err + } + + latestToken.SetAuthHeader(localVarRequest) + } + + // Basic HTTP Authentication + if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok { + localVarRequest.SetBasicAuth(auth.UserName, auth.Password) + } + + // AccessToken Authentication + if auth, ok := ctx.Value(ContextAccessToken).(string); ok { + localVarRequest.Header.Add("Authorization", "Bearer "+auth) + } + + } + + for header, value := range c.cfg.DefaultHeader { + localVarRequest.Header.Add(header, value) + } + return localVarRequest, nil +} + +func (c *APIClient) decode(v interface{}, b []byte, contentType string) (err error) { + if len(b) == 0 { + return nil + } + if s, ok := v.(*string); ok { + *s = string(b) + return nil + } + if xmlCheck.MatchString(contentType) { + if err = xml.Unmarshal(b, v); err != nil { + return err + } + return nil + } + if jsonCheck.MatchString(contentType) { + if actualObj, ok := v.(interface{ GetActualInstance() interface{} }); ok { // oneOf, anyOf schemas + if unmarshalObj, ok := actualObj.(interface{ UnmarshalJSON([]byte) error }); ok { // make sure it has UnmarshalJSON defined + if err = unmarshalObj.UnmarshalJSON(b); err != nil { + return err + } + } else { + return errors.New("Unknown type with GetActualInstance but no unmarshalObj.UnmarshalJSON defined") + } + } else if err = json.Unmarshal(b, v); err != nil { // simple model + return err + } + return nil + } + return errors.New("undefined response type") +} + +// Add a file to the multipart request +func addFile(w *multipart.Writer, fieldName, path string) error { + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + part, err := w.CreateFormFile(fieldName, filepath.Base(path)) + if err != nil { + return err + } + _, err = io.Copy(part, file) + + return err +} + +// Prevent trying to import "fmt" +func reportError(format string, a ...interface{}) error { + return fmt.Errorf(format, a...) +} + +// Prevent trying to import "bytes" +func newStrictDecoder(data []byte) *json.Decoder { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.DisallowUnknownFields() + return dec +} + +// Set request body from an interface{} +func setBody(body interface{}, contentType string) (bodyBuf *bytes.Buffer, err error) { + if bodyBuf == nil { + bodyBuf = &bytes.Buffer{} + } + + if reader, ok := body.(io.Reader); ok { + _, err = bodyBuf.ReadFrom(reader) + } else if b, ok := body.([]byte); ok { + _, err = bodyBuf.Write(b) + } else if s, ok := body.(string); ok { + _, err = bodyBuf.WriteString(s) + } else if s, ok := body.(*string); ok { + _, err = bodyBuf.WriteString(*s) + } else if jsonCheck.MatchString(contentType) { + err = json.NewEncoder(bodyBuf).Encode(body) + } else if xmlCheck.MatchString(contentType) { + err = xml.NewEncoder(bodyBuf).Encode(body) + } + + if err != nil { + return nil, err + } + + if bodyBuf.Len() == 0 { + err = fmt.Errorf("Invalid body type %s\n", contentType) + return nil, err + } + return bodyBuf, nil +} + +// detectContentType method is used to figure out `Request.Body` content type for request header +func detectContentType(body interface{}) string { + contentType := "text/plain; charset=utf-8" + kind := reflect.TypeOf(body).Kind() + + switch kind { + case reflect.Struct, reflect.Map, reflect.Ptr: + contentType = "application/json; charset=utf-8" + case reflect.String: + contentType = "text/plain; charset=utf-8" + default: + if b, ok := body.([]byte); ok { + contentType = http.DetectContentType(b) + } else if kind == reflect.Slice { + contentType = "application/json; charset=utf-8" + } + } + + return contentType +} + +// Ripped from https://github.com/gregjones/httpcache/blob/master/httpcache.go +type cacheControl map[string]string + +func parseCacheControl(headers http.Header) cacheControl { + cc := cacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc +} + +// CacheExpires helper function to determine remaining time before repeating a request. +func CacheExpires(r *http.Response) time.Time { + // Figure out when the cache expires. + var expires time.Time + now, err := time.Parse(time.RFC1123, r.Header.Get("date")) + if err != nil { + return time.Now() + } + respCacheControl := parseCacheControl(r.Header) + + if maxAge, ok := respCacheControl["max-age"]; ok { + lifetime, err := time.ParseDuration(maxAge + "s") + if err != nil { + expires = now + } else { + expires = now.Add(lifetime) + } + } else { + expiresHeader := r.Header.Get("Expires") + if expiresHeader != "" { + expires, err = time.Parse(time.RFC1123, expiresHeader) + if err != nil { + expires = now + } + } + } + return expires +} + +func strlen(s string) int { + return utf8.RuneCountInString(s) +} + +// GenericOpenAPIError Provides access to the body, error and model on returned errors. +type GenericOpenAPIError struct { + body []byte + error string + model interface{} +} + +// Error returns non-empty string if there was an error. +func (e GenericOpenAPIError) Error() string { + return e.error +} + +// Body returns the raw bytes of the response +func (e GenericOpenAPIError) Body() []byte { + return e.body +} + +// Model returns the unpacked model of the error +func (e GenericOpenAPIError) Model() interface{} { + return e.model +} diff --git a/internal/httpclient/configuration.go b/internal/httpclient/configuration.go new file mode 100644 index 0000000..204f505 --- /dev/null +++ b/internal/httpclient/configuration.go @@ -0,0 +1,230 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "context" + "fmt" + "net/http" + "strings" +) + +// contextKeys are used to identify the type of value in the context. +// Since these are string, it is possible to get a short description of the +// context key for logging and debugging using key.String(). + +type contextKey string + +func (c contextKey) String() string { + return "auth " + string(c) +} + +var ( + // ContextOAuth2 takes an oauth2.TokenSource as authentication for the request. + ContextOAuth2 = contextKey("token") + + // ContextBasicAuth takes BasicAuth as authentication for the request. + ContextBasicAuth = contextKey("basic") + + // ContextAccessToken takes a string oauth2 access token as authentication for the request. + ContextAccessToken = contextKey("accesstoken") + + // ContextAPIKeys takes a string apikey as authentication for the request + ContextAPIKeys = contextKey("apiKeys") + + // ContextHttpSignatureAuth takes HttpSignatureAuth as authentication for the request. + ContextHttpSignatureAuth = contextKey("httpsignature") + + // ContextServerIndex uses a server configuration from the index. + ContextServerIndex = contextKey("serverIndex") + + // ContextOperationServerIndices uses a server configuration from the index mapping. + ContextOperationServerIndices = contextKey("serverOperationIndices") + + // ContextServerVariables overrides a server configuration variables. + ContextServerVariables = contextKey("serverVariables") + + // ContextOperationServerVariables overrides a server configuration variables using operation specific values. + ContextOperationServerVariables = contextKey("serverOperationVariables") +) + +// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth +type BasicAuth struct { + UserName string `json:"userName,omitempty"` + Password string `json:"password,omitempty"` +} + +// APIKey provides API key based authentication to a request passed via context using ContextAPIKey +type APIKey struct { + Key string + Prefix string +} + +// ServerVariable stores the information about a server variable +type ServerVariable struct { + Description string + DefaultValue string + EnumValues []string +} + +// ServerConfiguration stores the information about a server +type ServerConfiguration struct { + URL string + Description string + Variables map[string]ServerVariable +} + +// ServerConfigurations stores multiple ServerConfiguration items +type ServerConfigurations []ServerConfiguration + +// Configuration stores the configuration of the API client +type Configuration struct { + Host string `json:"host,omitempty"` + Scheme string `json:"scheme,omitempty"` + DefaultHeader map[string]string `json:"defaultHeader,omitempty"` + UserAgent string `json:"userAgent,omitempty"` + Debug bool `json:"debug,omitempty"` + Servers ServerConfigurations + OperationServers map[string]ServerConfigurations + HTTPClient *http.Client +} + +// NewConfiguration returns a new Configuration object +func NewConfiguration() *Configuration { + cfg := &Configuration{ + DefaultHeader: make(map[string]string), + UserAgent: "OpenAPI-Generator/1.0.0/go", + Debug: false, + Servers: ServerConfigurations{ + { + URL: "", + Description: "No description provided", + }, + }, + OperationServers: map[string]ServerConfigurations{}, + } + return cfg +} + +// AddDefaultHeader adds a new HTTP header to the default header in the request +func (c *Configuration) AddDefaultHeader(key string, value string) { + c.DefaultHeader[key] = value +} + +// URL formats template on a index using given variables +func (sc ServerConfigurations) URL(index int, variables map[string]string) (string, error) { + if index < 0 || len(sc) <= index { + return "", fmt.Errorf("Index %v out of range %v", index, len(sc)-1) + } + server := sc[index] + url := server.URL + + // go through variables and replace placeholders + for name, variable := range server.Variables { + if value, ok := variables[name]; ok { + found := bool(len(variable.EnumValues) == 0) + for _, enumValue := range variable.EnumValues { + if value == enumValue { + found = true + } + } + if !found { + return "", fmt.Errorf("The variable %s in the server URL has invalid value %v. Must be %v", name, value, variable.EnumValues) + } + url = strings.Replace(url, "{"+name+"}", value, -1) + } else { + url = strings.Replace(url, "{"+name+"}", variable.DefaultValue, -1) + } + } + return url, nil +} + +// ServerURL returns URL based on server settings +func (c *Configuration) ServerURL(index int, variables map[string]string) (string, error) { + return c.Servers.URL(index, variables) +} + +func getServerIndex(ctx context.Context) (int, error) { + si := ctx.Value(ContextServerIndex) + if si != nil { + if index, ok := si.(int); ok { + return index, nil + } + return 0, reportError("Invalid type %T should be int", si) + } + return 0, nil +} + +func getServerOperationIndex(ctx context.Context, endpoint string) (int, error) { + osi := ctx.Value(ContextOperationServerIndices) + if osi != nil { + if operationIndices, ok := osi.(map[string]int); !ok { + return 0, reportError("Invalid type %T should be map[string]int", osi) + } else { + index, ok := operationIndices[endpoint] + if ok { + return index, nil + } + } + } + return getServerIndex(ctx) +} + +func getServerVariables(ctx context.Context) (map[string]string, error) { + sv := ctx.Value(ContextServerVariables) + if sv != nil { + if variables, ok := sv.(map[string]string); ok { + return variables, nil + } + return nil, reportError("ctx value of ContextServerVariables has invalid type %T should be map[string]string", sv) + } + return nil, nil +} + +func getServerOperationVariables(ctx context.Context, endpoint string) (map[string]string, error) { + osv := ctx.Value(ContextOperationServerVariables) + if osv != nil { + if operationVariables, ok := osv.(map[string]map[string]string); !ok { + return nil, reportError("ctx value of ContextOperationServerVariables has invalid type %T should be map[string]map[string]string", osv) + } else { + variables, ok := operationVariables[endpoint] + if ok { + return variables, nil + } + } + } + return getServerVariables(ctx) +} + +// ServerURLWithContext returns a new server URL given an endpoint +func (c *Configuration) ServerURLWithContext(ctx context.Context, endpoint string) (string, error) { + sc, ok := c.OperationServers[endpoint] + if !ok { + sc = c.Servers + } + + if ctx == nil { + return sc.URL(0, nil) + } + + index, err := getServerOperationIndex(ctx, endpoint) + if err != nil { + return "", err + } + + variables, err := getServerOperationVariables(ctx, endpoint) + if err != nil { + return "", err + } + + return sc.URL(index, variables) +} diff --git a/internal/httpclient/docs/BatchCheckPermissionBody.md b/internal/httpclient/docs/BatchCheckPermissionBody.md new file mode 100644 index 0000000..a91da96 --- /dev/null +++ b/internal/httpclient/docs/BatchCheckPermissionBody.md @@ -0,0 +1,56 @@ +# BatchCheckPermissionBody + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Tuples** | Pointer to [**[]Relationship**](Relationship.md) | | [optional] + +## Methods + +### NewBatchCheckPermissionBody + +`func NewBatchCheckPermissionBody() *BatchCheckPermissionBody` + +NewBatchCheckPermissionBody instantiates a new BatchCheckPermissionBody object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewBatchCheckPermissionBodyWithDefaults + +`func NewBatchCheckPermissionBodyWithDefaults() *BatchCheckPermissionBody` + +NewBatchCheckPermissionBodyWithDefaults instantiates a new BatchCheckPermissionBody object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetTuples + +`func (o *BatchCheckPermissionBody) GetTuples() []Relationship` + +GetTuples returns the Tuples field if non-nil, zero value otherwise. + +### GetTuplesOk + +`func (o *BatchCheckPermissionBody) GetTuplesOk() (*[]Relationship, bool)` + +GetTuplesOk returns a tuple with the Tuples field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetTuples + +`func (o *BatchCheckPermissionBody) SetTuples(v []Relationship)` + +SetTuples sets Tuples field to given value. + +### HasTuples + +`func (o *BatchCheckPermissionBody) HasTuples() bool` + +HasTuples returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/BatchCheckPermissionResult.md b/internal/httpclient/docs/BatchCheckPermissionResult.md new file mode 100644 index 0000000..f11faf1 --- /dev/null +++ b/internal/httpclient/docs/BatchCheckPermissionResult.md @@ -0,0 +1,51 @@ +# BatchCheckPermissionResult + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Results** | [**[]CheckPermissionResultWithError**](CheckPermissionResultWithError.md) | An array of check results. The order aligns with the input order. | + +## Methods + +### NewBatchCheckPermissionResult + +`func NewBatchCheckPermissionResult(results []CheckPermissionResultWithError, ) *BatchCheckPermissionResult` + +NewBatchCheckPermissionResult instantiates a new BatchCheckPermissionResult object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewBatchCheckPermissionResultWithDefaults + +`func NewBatchCheckPermissionResultWithDefaults() *BatchCheckPermissionResult` + +NewBatchCheckPermissionResultWithDefaults instantiates a new BatchCheckPermissionResult object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetResults + +`func (o *BatchCheckPermissionResult) GetResults() []CheckPermissionResultWithError` + +GetResults returns the Results field if non-nil, zero value otherwise. + +### GetResultsOk + +`func (o *BatchCheckPermissionResult) GetResultsOk() (*[]CheckPermissionResultWithError, bool)` + +GetResultsOk returns a tuple with the Results field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetResults + +`func (o *BatchCheckPermissionResult) SetResults(v []CheckPermissionResultWithError)` + +SetResults sets Results field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/CheckOplSyntaxResult.md b/internal/httpclient/docs/CheckOplSyntaxResult.md new file mode 100644 index 0000000..38cbe37 --- /dev/null +++ b/internal/httpclient/docs/CheckOplSyntaxResult.md @@ -0,0 +1,56 @@ +# CheckOplSyntaxResult + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Errors** | Pointer to [**[]ParseError**](ParseError.md) | The list of syntax errors | [optional] + +## Methods + +### NewCheckOplSyntaxResult + +`func NewCheckOplSyntaxResult() *CheckOplSyntaxResult` + +NewCheckOplSyntaxResult instantiates a new CheckOplSyntaxResult object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewCheckOplSyntaxResultWithDefaults + +`func NewCheckOplSyntaxResultWithDefaults() *CheckOplSyntaxResult` + +NewCheckOplSyntaxResultWithDefaults instantiates a new CheckOplSyntaxResult object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetErrors + +`func (o *CheckOplSyntaxResult) GetErrors() []ParseError` + +GetErrors returns the Errors field if non-nil, zero value otherwise. + +### GetErrorsOk + +`func (o *CheckOplSyntaxResult) GetErrorsOk() (*[]ParseError, bool)` + +GetErrorsOk returns a tuple with the Errors field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetErrors + +`func (o *CheckOplSyntaxResult) SetErrors(v []ParseError)` + +SetErrors sets Errors field to given value. + +### HasErrors + +`func (o *CheckOplSyntaxResult) HasErrors() bool` + +HasErrors returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/CheckPermissionResult.md b/internal/httpclient/docs/CheckPermissionResult.md new file mode 100644 index 0000000..7270de6 --- /dev/null +++ b/internal/httpclient/docs/CheckPermissionResult.md @@ -0,0 +1,51 @@ +# CheckPermissionResult + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Allowed** | **bool** | whether the relation tuple is allowed | + +## Methods + +### NewCheckPermissionResult + +`func NewCheckPermissionResult(allowed bool, ) *CheckPermissionResult` + +NewCheckPermissionResult instantiates a new CheckPermissionResult object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewCheckPermissionResultWithDefaults + +`func NewCheckPermissionResultWithDefaults() *CheckPermissionResult` + +NewCheckPermissionResultWithDefaults instantiates a new CheckPermissionResult object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetAllowed + +`func (o *CheckPermissionResult) GetAllowed() bool` + +GetAllowed returns the Allowed field if non-nil, zero value otherwise. + +### GetAllowedOk + +`func (o *CheckPermissionResult) GetAllowedOk() (*bool, bool)` + +GetAllowedOk returns a tuple with the Allowed field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetAllowed + +`func (o *CheckPermissionResult) SetAllowed(v bool)` + +SetAllowed sets Allowed field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/CheckPermissionResultWithError.md b/internal/httpclient/docs/CheckPermissionResultWithError.md new file mode 100644 index 0000000..9f2eec3 --- /dev/null +++ b/internal/httpclient/docs/CheckPermissionResultWithError.md @@ -0,0 +1,77 @@ +# CheckPermissionResultWithError + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Allowed** | **bool** | whether the relation tuple is allowed | +**Error** | Pointer to **string** | any error generated while checking the relation tuple | [optional] + +## Methods + +### NewCheckPermissionResultWithError + +`func NewCheckPermissionResultWithError(allowed bool, ) *CheckPermissionResultWithError` + +NewCheckPermissionResultWithError instantiates a new CheckPermissionResultWithError object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewCheckPermissionResultWithErrorWithDefaults + +`func NewCheckPermissionResultWithErrorWithDefaults() *CheckPermissionResultWithError` + +NewCheckPermissionResultWithErrorWithDefaults instantiates a new CheckPermissionResultWithError object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetAllowed + +`func (o *CheckPermissionResultWithError) GetAllowed() bool` + +GetAllowed returns the Allowed field if non-nil, zero value otherwise. + +### GetAllowedOk + +`func (o *CheckPermissionResultWithError) GetAllowedOk() (*bool, bool)` + +GetAllowedOk returns a tuple with the Allowed field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetAllowed + +`func (o *CheckPermissionResultWithError) SetAllowed(v bool)` + +SetAllowed sets Allowed field to given value. + + +### GetError + +`func (o *CheckPermissionResultWithError) GetError() string` + +GetError returns the Error field if non-nil, zero value otherwise. + +### GetErrorOk + +`func (o *CheckPermissionResultWithError) GetErrorOk() (*string, bool)` + +GetErrorOk returns a tuple with the Error field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetError + +`func (o *CheckPermissionResultWithError) SetError(v string)` + +SetError sets Error field to given value. + +### HasError + +`func (o *CheckPermissionResultWithError) HasError() bool` + +HasError returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/CreateRelationshipBody.md b/internal/httpclient/docs/CreateRelationshipBody.md new file mode 100644 index 0000000..91bb3a7 --- /dev/null +++ b/internal/httpclient/docs/CreateRelationshipBody.md @@ -0,0 +1,160 @@ +# CreateRelationshipBody + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | Pointer to **string** | Namespace to query | [optional] +**Object** | Pointer to **string** | Object to query | [optional] +**Relation** | Pointer to **string** | Relation to query | [optional] +**SubjectId** | Pointer to **string** | SubjectID to query Either SubjectSet or SubjectID can be provided. | [optional] +**SubjectSet** | Pointer to [**SubjectSet**](SubjectSet.md) | | [optional] + +## Methods + +### NewCreateRelationshipBody + +`func NewCreateRelationshipBody() *CreateRelationshipBody` + +NewCreateRelationshipBody instantiates a new CreateRelationshipBody object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewCreateRelationshipBodyWithDefaults + +`func NewCreateRelationshipBodyWithDefaults() *CreateRelationshipBody` + +NewCreateRelationshipBodyWithDefaults instantiates a new CreateRelationshipBody object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *CreateRelationshipBody) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *CreateRelationshipBody) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *CreateRelationshipBody) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + +### HasNamespace + +`func (o *CreateRelationshipBody) HasNamespace() bool` + +HasNamespace returns a boolean if a field has been set. + +### GetObject + +`func (o *CreateRelationshipBody) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *CreateRelationshipBody) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *CreateRelationshipBody) SetObject(v string)` + +SetObject sets Object field to given value. + +### HasObject + +`func (o *CreateRelationshipBody) HasObject() bool` + +HasObject returns a boolean if a field has been set. + +### GetRelation + +`func (o *CreateRelationshipBody) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *CreateRelationshipBody) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *CreateRelationshipBody) SetRelation(v string)` + +SetRelation sets Relation field to given value. + +### HasRelation + +`func (o *CreateRelationshipBody) HasRelation() bool` + +HasRelation returns a boolean if a field has been set. + +### GetSubjectId + +`func (o *CreateRelationshipBody) GetSubjectId() string` + +GetSubjectId returns the SubjectId field if non-nil, zero value otherwise. + +### GetSubjectIdOk + +`func (o *CreateRelationshipBody) GetSubjectIdOk() (*string, bool)` + +GetSubjectIdOk returns a tuple with the SubjectId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectId + +`func (o *CreateRelationshipBody) SetSubjectId(v string)` + +SetSubjectId sets SubjectId field to given value. + +### HasSubjectId + +`func (o *CreateRelationshipBody) HasSubjectId() bool` + +HasSubjectId returns a boolean if a field has been set. + +### GetSubjectSet + +`func (o *CreateRelationshipBody) GetSubjectSet() SubjectSet` + +GetSubjectSet returns the SubjectSet field if non-nil, zero value otherwise. + +### GetSubjectSetOk + +`func (o *CreateRelationshipBody) GetSubjectSetOk() (*SubjectSet, bool)` + +GetSubjectSetOk returns a tuple with the SubjectSet field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectSet + +`func (o *CreateRelationshipBody) SetSubjectSet(v SubjectSet)` + +SetSubjectSet sets SubjectSet field to given value. + +### HasSubjectSet + +`func (o *CreateRelationshipBody) HasSubjectSet() bool` + +HasSubjectSet returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/ErrorGeneric.md b/internal/httpclient/docs/ErrorGeneric.md new file mode 100644 index 0000000..a340c2c --- /dev/null +++ b/internal/httpclient/docs/ErrorGeneric.md @@ -0,0 +1,51 @@ +# ErrorGeneric + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Error** | [**GenericError**](GenericError.md) | | + +## Methods + +### NewErrorGeneric + +`func NewErrorGeneric(error_ GenericError, ) *ErrorGeneric` + +NewErrorGeneric instantiates a new ErrorGeneric object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewErrorGenericWithDefaults + +`func NewErrorGenericWithDefaults() *ErrorGeneric` + +NewErrorGenericWithDefaults instantiates a new ErrorGeneric object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetError + +`func (o *ErrorGeneric) GetError() GenericError` + +GetError returns the Error field if non-nil, zero value otherwise. + +### GetErrorOk + +`func (o *ErrorGeneric) GetErrorOk() (*GenericError, bool)` + +GetErrorOk returns a tuple with the Error field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetError + +`func (o *ErrorGeneric) SetError(v GenericError)` + +SetError sets Error field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/ExpandedPermissionTree.md b/internal/httpclient/docs/ExpandedPermissionTree.md new file mode 100644 index 0000000..3c08782 --- /dev/null +++ b/internal/httpclient/docs/ExpandedPermissionTree.md @@ -0,0 +1,103 @@ +# ExpandedPermissionTree + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Children** | Pointer to [**[]ExpandedPermissionTree**](ExpandedPermissionTree.md) | The children of the node, possibly none. | [optional] +**Tuple** | Pointer to [**Relationship**](Relationship.md) | | [optional] +**Type** | **string** | The type of the node. union TreeNodeUnion exclusion TreeNodeExclusion intersection TreeNodeIntersection leaf TreeNodeLeaf tuple_to_subject_set TreeNodeTupleToSubjectSet computed_subject_set TreeNodeComputedSubjectSet not TreeNodeNot unspecified TreeNodeUnspecified | + +## Methods + +### NewExpandedPermissionTree + +`func NewExpandedPermissionTree(type_ string, ) *ExpandedPermissionTree` + +NewExpandedPermissionTree instantiates a new ExpandedPermissionTree object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewExpandedPermissionTreeWithDefaults + +`func NewExpandedPermissionTreeWithDefaults() *ExpandedPermissionTree` + +NewExpandedPermissionTreeWithDefaults instantiates a new ExpandedPermissionTree object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetChildren + +`func (o *ExpandedPermissionTree) GetChildren() []ExpandedPermissionTree` + +GetChildren returns the Children field if non-nil, zero value otherwise. + +### GetChildrenOk + +`func (o *ExpandedPermissionTree) GetChildrenOk() (*[]ExpandedPermissionTree, bool)` + +GetChildrenOk returns a tuple with the Children field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetChildren + +`func (o *ExpandedPermissionTree) SetChildren(v []ExpandedPermissionTree)` + +SetChildren sets Children field to given value. + +### HasChildren + +`func (o *ExpandedPermissionTree) HasChildren() bool` + +HasChildren returns a boolean if a field has been set. + +### GetTuple + +`func (o *ExpandedPermissionTree) GetTuple() Relationship` + +GetTuple returns the Tuple field if non-nil, zero value otherwise. + +### GetTupleOk + +`func (o *ExpandedPermissionTree) GetTupleOk() (*Relationship, bool)` + +GetTupleOk returns a tuple with the Tuple field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetTuple + +`func (o *ExpandedPermissionTree) SetTuple(v Relationship)` + +SetTuple sets Tuple field to given value. + +### HasTuple + +`func (o *ExpandedPermissionTree) HasTuple() bool` + +HasTuple returns a boolean if a field has been set. + +### GetType + +`func (o *ExpandedPermissionTree) GetType() string` + +GetType returns the Type field if non-nil, zero value otherwise. + +### GetTypeOk + +`func (o *ExpandedPermissionTree) GetTypeOk() (*string, bool)` + +GetTypeOk returns a tuple with the Type field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetType + +`func (o *ExpandedPermissionTree) SetType(v string)` + +SetType sets Type field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/GenericError.md b/internal/httpclient/docs/GenericError.md new file mode 100644 index 0000000..5a09d73 --- /dev/null +++ b/internal/httpclient/docs/GenericError.md @@ -0,0 +1,233 @@ +# GenericError + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Code** | Pointer to **int64** | The status code | [optional] +**Debug** | Pointer to **string** | Debug information This field is often not exposed to protect against leaking sensitive information. | [optional] +**Details** | Pointer to **map[string]interface{}** | Further error details | [optional] +**Id** | Pointer to **string** | The error ID Useful when trying to identify various errors in application logic. | [optional] +**Message** | **string** | Error message The error's message. | +**Reason** | Pointer to **string** | A human-readable reason for the error | [optional] +**Request** | Pointer to **string** | The request ID The request ID is often exposed internally in order to trace errors across service architectures. This is often a UUID. | [optional] +**Status** | Pointer to **string** | The status description | [optional] + +## Methods + +### NewGenericError + +`func NewGenericError(message string, ) *GenericError` + +NewGenericError instantiates a new GenericError object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewGenericErrorWithDefaults + +`func NewGenericErrorWithDefaults() *GenericError` + +NewGenericErrorWithDefaults instantiates a new GenericError object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetCode + +`func (o *GenericError) GetCode() int64` + +GetCode returns the Code field if non-nil, zero value otherwise. + +### GetCodeOk + +`func (o *GenericError) GetCodeOk() (*int64, bool)` + +GetCodeOk returns a tuple with the Code field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCode + +`func (o *GenericError) SetCode(v int64)` + +SetCode sets Code field to given value. + +### HasCode + +`func (o *GenericError) HasCode() bool` + +HasCode returns a boolean if a field has been set. + +### GetDebug + +`func (o *GenericError) GetDebug() string` + +GetDebug returns the Debug field if non-nil, zero value otherwise. + +### GetDebugOk + +`func (o *GenericError) GetDebugOk() (*string, bool)` + +GetDebugOk returns a tuple with the Debug field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDebug + +`func (o *GenericError) SetDebug(v string)` + +SetDebug sets Debug field to given value. + +### HasDebug + +`func (o *GenericError) HasDebug() bool` + +HasDebug returns a boolean if a field has been set. + +### GetDetails + +`func (o *GenericError) GetDetails() map[string]interface{}` + +GetDetails returns the Details field if non-nil, zero value otherwise. + +### GetDetailsOk + +`func (o *GenericError) GetDetailsOk() (*map[string]interface{}, bool)` + +GetDetailsOk returns a tuple with the Details field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDetails + +`func (o *GenericError) SetDetails(v map[string]interface{})` + +SetDetails sets Details field to given value. + +### HasDetails + +`func (o *GenericError) HasDetails() bool` + +HasDetails returns a boolean if a field has been set. + +### GetId + +`func (o *GenericError) GetId() string` + +GetId returns the Id field if non-nil, zero value otherwise. + +### GetIdOk + +`func (o *GenericError) GetIdOk() (*string, bool)` + +GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetId + +`func (o *GenericError) SetId(v string)` + +SetId sets Id field to given value. + +### HasId + +`func (o *GenericError) HasId() bool` + +HasId returns a boolean if a field has been set. + +### GetMessage + +`func (o *GenericError) GetMessage() string` + +GetMessage returns the Message field if non-nil, zero value otherwise. + +### GetMessageOk + +`func (o *GenericError) GetMessageOk() (*string, bool)` + +GetMessageOk returns a tuple with the Message field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMessage + +`func (o *GenericError) SetMessage(v string)` + +SetMessage sets Message field to given value. + + +### GetReason + +`func (o *GenericError) GetReason() string` + +GetReason returns the Reason field if non-nil, zero value otherwise. + +### GetReasonOk + +`func (o *GenericError) GetReasonOk() (*string, bool)` + +GetReasonOk returns a tuple with the Reason field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetReason + +`func (o *GenericError) SetReason(v string)` + +SetReason sets Reason field to given value. + +### HasReason + +`func (o *GenericError) HasReason() bool` + +HasReason returns a boolean if a field has been set. + +### GetRequest + +`func (o *GenericError) GetRequest() string` + +GetRequest returns the Request field if non-nil, zero value otherwise. + +### GetRequestOk + +`func (o *GenericError) GetRequestOk() (*string, bool)` + +GetRequestOk returns a tuple with the Request field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRequest + +`func (o *GenericError) SetRequest(v string)` + +SetRequest sets Request field to given value. + +### HasRequest + +`func (o *GenericError) HasRequest() bool` + +HasRequest returns a boolean if a field has been set. + +### GetStatus + +`func (o *GenericError) GetStatus() string` + +GetStatus returns the Status field if non-nil, zero value otherwise. + +### GetStatusOk + +`func (o *GenericError) GetStatusOk() (*string, bool)` + +GetStatusOk returns a tuple with the Status field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetStatus + +`func (o *GenericError) SetStatus(v string)` + +SetStatus sets Status field to given value. + +### HasStatus + +`func (o *GenericError) HasStatus() bool` + +HasStatus returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/GetVersion200Response.md b/internal/httpclient/docs/GetVersion200Response.md new file mode 100644 index 0000000..30cbcf9 --- /dev/null +++ b/internal/httpclient/docs/GetVersion200Response.md @@ -0,0 +1,51 @@ +# GetVersion200Response + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Version** | **string** | The version of Ory Keto. | + +## Methods + +### NewGetVersion200Response + +`func NewGetVersion200Response(version string, ) *GetVersion200Response` + +NewGetVersion200Response instantiates a new GetVersion200Response object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewGetVersion200ResponseWithDefaults + +`func NewGetVersion200ResponseWithDefaults() *GetVersion200Response` + +NewGetVersion200ResponseWithDefaults instantiates a new GetVersion200Response object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetVersion + +`func (o *GetVersion200Response) GetVersion() string` + +GetVersion returns the Version field if non-nil, zero value otherwise. + +### GetVersionOk + +`func (o *GetVersion200Response) GetVersionOk() (*string, bool)` + +GetVersionOk returns a tuple with the Version field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVersion + +`func (o *GetVersion200Response) SetVersion(v string)` + +SetVersion sets Version field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/HealthNotReadyStatus.md b/internal/httpclient/docs/HealthNotReadyStatus.md new file mode 100644 index 0000000..9c40dcf --- /dev/null +++ b/internal/httpclient/docs/HealthNotReadyStatus.md @@ -0,0 +1,56 @@ +# HealthNotReadyStatus + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Errors** | Pointer to **map[string]string** | Errors contains a list of errors that caused the not ready status. | [optional] + +## Methods + +### NewHealthNotReadyStatus + +`func NewHealthNotReadyStatus() *HealthNotReadyStatus` + +NewHealthNotReadyStatus instantiates a new HealthNotReadyStatus object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewHealthNotReadyStatusWithDefaults + +`func NewHealthNotReadyStatusWithDefaults() *HealthNotReadyStatus` + +NewHealthNotReadyStatusWithDefaults instantiates a new HealthNotReadyStatus object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetErrors + +`func (o *HealthNotReadyStatus) GetErrors() map[string]string` + +GetErrors returns the Errors field if non-nil, zero value otherwise. + +### GetErrorsOk + +`func (o *HealthNotReadyStatus) GetErrorsOk() (*map[string]string, bool)` + +GetErrorsOk returns a tuple with the Errors field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetErrors + +`func (o *HealthNotReadyStatus) SetErrors(v map[string]string)` + +SetErrors sets Errors field to given value. + +### HasErrors + +`func (o *HealthNotReadyStatus) HasErrors() bool` + +HasErrors returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/HealthStatus.md b/internal/httpclient/docs/HealthStatus.md new file mode 100644 index 0000000..0173b42 --- /dev/null +++ b/internal/httpclient/docs/HealthStatus.md @@ -0,0 +1,56 @@ +# HealthStatus + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Status** | Pointer to **string** | Status always contains \"ok\". | [optional] + +## Methods + +### NewHealthStatus + +`func NewHealthStatus() *HealthStatus` + +NewHealthStatus instantiates a new HealthStatus object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewHealthStatusWithDefaults + +`func NewHealthStatusWithDefaults() *HealthStatus` + +NewHealthStatusWithDefaults instantiates a new HealthStatus object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetStatus + +`func (o *HealthStatus) GetStatus() string` + +GetStatus returns the Status field if non-nil, zero value otherwise. + +### GetStatusOk + +`func (o *HealthStatus) GetStatusOk() (*string, bool)` + +GetStatusOk returns a tuple with the Status field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetStatus + +`func (o *HealthStatus) SetStatus(v string)` + +SetStatus sets Status field to given value. + +### HasStatus + +`func (o *HealthStatus) HasStatus() bool` + +HasStatus returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/IsAlive200Response.md b/internal/httpclient/docs/IsAlive200Response.md new file mode 100644 index 0000000..e398fb1 --- /dev/null +++ b/internal/httpclient/docs/IsAlive200Response.md @@ -0,0 +1,51 @@ +# IsAlive200Response + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Status** | **string** | Always \"ok\". | + +## Methods + +### NewIsAlive200Response + +`func NewIsAlive200Response(status string, ) *IsAlive200Response` + +NewIsAlive200Response instantiates a new IsAlive200Response object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewIsAlive200ResponseWithDefaults + +`func NewIsAlive200ResponseWithDefaults() *IsAlive200Response` + +NewIsAlive200ResponseWithDefaults instantiates a new IsAlive200Response object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetStatus + +`func (o *IsAlive200Response) GetStatus() string` + +GetStatus returns the Status field if non-nil, zero value otherwise. + +### GetStatusOk + +`func (o *IsAlive200Response) GetStatusOk() (*string, bool)` + +GetStatusOk returns a tuple with the Status field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetStatus + +`func (o *IsAlive200Response) SetStatus(v string)` + +SetStatus sets Status field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/IsReady503Response.md b/internal/httpclient/docs/IsReady503Response.md new file mode 100644 index 0000000..3297fd2 --- /dev/null +++ b/internal/httpclient/docs/IsReady503Response.md @@ -0,0 +1,51 @@ +# IsReady503Response + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Errors** | **map[string]string** | Errors contains a list of errors that caused the not ready status. | + +## Methods + +### NewIsReady503Response + +`func NewIsReady503Response(errors map[string]string, ) *IsReady503Response` + +NewIsReady503Response instantiates a new IsReady503Response object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewIsReady503ResponseWithDefaults + +`func NewIsReady503ResponseWithDefaults() *IsReady503Response` + +NewIsReady503ResponseWithDefaults instantiates a new IsReady503Response object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetErrors + +`func (o *IsReady503Response) GetErrors() map[string]string` + +GetErrors returns the Errors field if non-nil, zero value otherwise. + +### GetErrorsOk + +`func (o *IsReady503Response) GetErrorsOk() (*map[string]string, bool)` + +GetErrorsOk returns a tuple with the Errors field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetErrors + +`func (o *IsReady503Response) SetErrors(v map[string]string)` + +SetErrors sets Errors field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/MetadataApi.md b/internal/httpclient/docs/MetadataApi.md new file mode 100644 index 0000000..a6c1a8e --- /dev/null +++ b/internal/httpclient/docs/MetadataApi.md @@ -0,0 +1,194 @@ +# \MetadataApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**GetVersion**](MetadataApi.md#GetVersion) | **Get** /version | Return Running Software Version. +[**IsAlive**](MetadataApi.md#IsAlive) | **Get** /health/alive | Check HTTP Server Status +[**IsReady**](MetadataApi.md#IsReady) | **Get** /health/ready | Check HTTP Server and Database Status + + + +## GetVersion + +> GetVersion200Response GetVersion(ctx).Execute() + +Return Running Software Version. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.MetadataApi.GetVersion(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `MetadataApi.GetVersion``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `GetVersion`: GetVersion200Response + fmt.Fprintf(os.Stdout, "Response from `MetadataApi.GetVersion`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiGetVersionRequest struct via the builder pattern + + +### Return type + +[**GetVersion200Response**](GetVersion200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## IsAlive + +> IsAlive200Response IsAlive(ctx).Execute() + +Check HTTP Server Status + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.MetadataApi.IsAlive(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `MetadataApi.IsAlive``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `IsAlive`: IsAlive200Response + fmt.Fprintf(os.Stdout, "Response from `MetadataApi.IsAlive`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiIsAliveRequest struct via the builder pattern + + +### Return type + +[**IsAlive200Response**](IsAlive200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json, text/plain + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## IsReady + +> IsAlive200Response IsReady(ctx).Execute() + +Check HTTP Server and Database Status + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.MetadataApi.IsReady(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `MetadataApi.IsReady``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `IsReady`: IsAlive200Response + fmt.Fprintf(os.Stdout, "Response from `MetadataApi.IsReady`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiIsReadyRequest struct via the builder pattern + + +### Return type + +[**IsAlive200Response**](IsAlive200Response.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json, text/plain + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + diff --git a/internal/httpclient/docs/Namespace.md b/internal/httpclient/docs/Namespace.md new file mode 100644 index 0000000..41c37de --- /dev/null +++ b/internal/httpclient/docs/Namespace.md @@ -0,0 +1,56 @@ +# Namespace + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Name** | Pointer to **string** | Name of the namespace. | [optional] + +## Methods + +### NewNamespace + +`func NewNamespace() *Namespace` + +NewNamespace instantiates a new Namespace object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewNamespaceWithDefaults + +`func NewNamespaceWithDefaults() *Namespace` + +NewNamespaceWithDefaults instantiates a new Namespace object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetName + +`func (o *Namespace) GetName() string` + +GetName returns the Name field if non-nil, zero value otherwise. + +### GetNameOk + +`func (o *Namespace) GetNameOk() (*string, bool)` + +GetNameOk returns a tuple with the Name field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetName + +`func (o *Namespace) SetName(v string)` + +SetName sets Name field to given value. + +### HasName + +`func (o *Namespace) HasName() bool` + +HasName returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/ParseError.md b/internal/httpclient/docs/ParseError.md new file mode 100644 index 0000000..9d04633 --- /dev/null +++ b/internal/httpclient/docs/ParseError.md @@ -0,0 +1,108 @@ +# ParseError + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**End** | Pointer to [**SourcePosition**](SourcePosition.md) | | [optional] +**Message** | Pointer to **string** | | [optional] +**Start** | Pointer to [**SourcePosition**](SourcePosition.md) | | [optional] + +## Methods + +### NewParseError + +`func NewParseError() *ParseError` + +NewParseError instantiates a new ParseError object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewParseErrorWithDefaults + +`func NewParseErrorWithDefaults() *ParseError` + +NewParseErrorWithDefaults instantiates a new ParseError object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetEnd + +`func (o *ParseError) GetEnd() SourcePosition` + +GetEnd returns the End field if non-nil, zero value otherwise. + +### GetEndOk + +`func (o *ParseError) GetEndOk() (*SourcePosition, bool)` + +GetEndOk returns a tuple with the End field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetEnd + +`func (o *ParseError) SetEnd(v SourcePosition)` + +SetEnd sets End field to given value. + +### HasEnd + +`func (o *ParseError) HasEnd() bool` + +HasEnd returns a boolean if a field has been set. + +### GetMessage + +`func (o *ParseError) GetMessage() string` + +GetMessage returns the Message field if non-nil, zero value otherwise. + +### GetMessageOk + +`func (o *ParseError) GetMessageOk() (*string, bool)` + +GetMessageOk returns a tuple with the Message field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMessage + +`func (o *ParseError) SetMessage(v string)` + +SetMessage sets Message field to given value. + +### HasMessage + +`func (o *ParseError) HasMessage() bool` + +HasMessage returns a boolean if a field has been set. + +### GetStart + +`func (o *ParseError) GetStart() SourcePosition` + +GetStart returns the Start field if non-nil, zero value otherwise. + +### GetStartOk + +`func (o *ParseError) GetStartOk() (*SourcePosition, bool)` + +GetStartOk returns a tuple with the Start field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetStart + +`func (o *ParseError) SetStart(v SourcePosition)` + +SetStart sets Start field to given value. + +### HasStart + +`func (o *ParseError) HasStart() bool` + +HasStart returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/PermissionApi.md b/internal/httpclient/docs/PermissionApi.md new file mode 100644 index 0000000..416ea2b --- /dev/null +++ b/internal/httpclient/docs/PermissionApi.md @@ -0,0 +1,450 @@ +# \PermissionApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**BatchCheckPermission**](PermissionApi.md#BatchCheckPermission) | **Post** /relation-tuples/batch/check | Batch check permissions +[**CheckPermission**](PermissionApi.md#CheckPermission) | **Get** /relation-tuples/check/openapi | Check a permission +[**CheckPermissionOrError**](PermissionApi.md#CheckPermissionOrError) | **Get** /relation-tuples/check | Check a permission +[**ExpandPermissions**](PermissionApi.md#ExpandPermissions) | **Get** /relation-tuples/expand | Expand a Relationship into permissions. +[**PostCheckPermission**](PermissionApi.md#PostCheckPermission) | **Post** /relation-tuples/check/openapi | Check a permission +[**PostCheckPermissionOrError**](PermissionApi.md#PostCheckPermissionOrError) | **Post** /relation-tuples/check | Check a permission + + + +## BatchCheckPermission + +> BatchCheckPermissionResult BatchCheckPermission(ctx).MaxDepth(maxDepth).BatchCheckPermissionBody(batchCheckPermissionBody).Execute() + +Batch check permissions + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + maxDepth := int64(789) // int64 | (optional) + batchCheckPermissionBody := *openapiclient.NewBatchCheckPermissionBody() // BatchCheckPermissionBody | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.BatchCheckPermission(context.Background()).MaxDepth(maxDepth).BatchCheckPermissionBody(batchCheckPermissionBody).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.BatchCheckPermission``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `BatchCheckPermission`: BatchCheckPermissionResult + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.BatchCheckPermission`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiBatchCheckPermissionRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **maxDepth** | **int64** | | + **batchCheckPermissionBody** | [**BatchCheckPermissionBody**](BatchCheckPermissionBody.md) | | + +### Return type + +[**BatchCheckPermissionResult**](BatchCheckPermissionResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## CheckPermission + +> CheckPermissionResult CheckPermission(ctx).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).MaxDepth(maxDepth).Execute() + +Check a permission + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + namespace := "namespace_example" // string | Namespace of the Relationship (optional) + object := "object_example" // string | Object of the Relationship (optional) + relation := "relation_example" // string | Relation of the Relationship (optional) + subjectId := "subjectId_example" // string | SubjectID of the Relationship (optional) + subjectSetNamespace := "subjectSetNamespace_example" // string | Namespace of the Subject Set (optional) + subjectSetObject := "subjectSetObject_example" // string | Object of the Subject Set (optional) + subjectSetRelation := "subjectSetRelation_example" // string | Relation of the Subject Set (optional) + maxDepth := int64(789) // int64 | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.CheckPermission(context.Background()).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).MaxDepth(maxDepth).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.CheckPermission``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `CheckPermission`: CheckPermissionResult + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.CheckPermission`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiCheckPermissionRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **namespace** | **string** | Namespace of the Relationship | + **object** | **string** | Object of the Relationship | + **relation** | **string** | Relation of the Relationship | + **subjectId** | **string** | SubjectID of the Relationship | + **subjectSetNamespace** | **string** | Namespace of the Subject Set | + **subjectSetObject** | **string** | Object of the Subject Set | + **subjectSetRelation** | **string** | Relation of the Subject Set | + **maxDepth** | **int64** | | + +### Return type + +[**CheckPermissionResult**](CheckPermissionResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## CheckPermissionOrError + +> CheckPermissionResult CheckPermissionOrError(ctx).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).MaxDepth(maxDepth).Execute() + +Check a permission + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + namespace := "namespace_example" // string | Namespace of the Relationship (optional) + object := "object_example" // string | Object of the Relationship (optional) + relation := "relation_example" // string | Relation of the Relationship (optional) + subjectId := "subjectId_example" // string | SubjectID of the Relationship (optional) + subjectSetNamespace := "subjectSetNamespace_example" // string | Namespace of the Subject Set (optional) + subjectSetObject := "subjectSetObject_example" // string | Object of the Subject Set (optional) + subjectSetRelation := "subjectSetRelation_example" // string | Relation of the Subject Set (optional) + maxDepth := int64(789) // int64 | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.CheckPermissionOrError(context.Background()).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).MaxDepth(maxDepth).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.CheckPermissionOrError``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `CheckPermissionOrError`: CheckPermissionResult + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.CheckPermissionOrError`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiCheckPermissionOrErrorRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **namespace** | **string** | Namespace of the Relationship | + **object** | **string** | Object of the Relationship | + **relation** | **string** | Relation of the Relationship | + **subjectId** | **string** | SubjectID of the Relationship | + **subjectSetNamespace** | **string** | Namespace of the Subject Set | + **subjectSetObject** | **string** | Object of the Subject Set | + **subjectSetRelation** | **string** | Relation of the Subject Set | + **maxDepth** | **int64** | | + +### Return type + +[**CheckPermissionResult**](CheckPermissionResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## ExpandPermissions + +> ExpandedPermissionTree ExpandPermissions(ctx).Namespace(namespace).Object(object).Relation(relation).MaxDepth(maxDepth).Execute() + +Expand a Relationship into permissions. + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + namespace := "namespace_example" // string | Namespace of the Subject Set + object := "object_example" // string | Object of the Subject Set + relation := "relation_example" // string | Relation of the Subject Set + maxDepth := int64(789) // int64 | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.ExpandPermissions(context.Background()).Namespace(namespace).Object(object).Relation(relation).MaxDepth(maxDepth).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.ExpandPermissions``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `ExpandPermissions`: ExpandedPermissionTree + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.ExpandPermissions`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiExpandPermissionsRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **namespace** | **string** | Namespace of the Subject Set | + **object** | **string** | Object of the Subject Set | + **relation** | **string** | Relation of the Subject Set | + **maxDepth** | **int64** | | + +### Return type + +[**ExpandedPermissionTree**](ExpandedPermissionTree.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## PostCheckPermission + +> CheckPermissionResult PostCheckPermission(ctx).MaxDepth(maxDepth).PostCheckPermissionBody(postCheckPermissionBody).Execute() + +Check a permission + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + maxDepth := int64(789) // int64 | (optional) + postCheckPermissionBody := *openapiclient.NewPostCheckPermissionBody() // PostCheckPermissionBody | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.PostCheckPermission(context.Background()).MaxDepth(maxDepth).PostCheckPermissionBody(postCheckPermissionBody).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.PostCheckPermission``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `PostCheckPermission`: CheckPermissionResult + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.PostCheckPermission`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiPostCheckPermissionRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **maxDepth** | **int64** | | + **postCheckPermissionBody** | [**PostCheckPermissionBody**](PostCheckPermissionBody.md) | | + +### Return type + +[**CheckPermissionResult**](CheckPermissionResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## PostCheckPermissionOrError + +> CheckPermissionResult PostCheckPermissionOrError(ctx).MaxDepth(maxDepth).PostCheckPermissionOrErrorBody(postCheckPermissionOrErrorBody).Execute() + +Check a permission + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + maxDepth := int64(789) // int64 | (optional) + postCheckPermissionOrErrorBody := *openapiclient.NewPostCheckPermissionOrErrorBody() // PostCheckPermissionOrErrorBody | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.PermissionApi.PostCheckPermissionOrError(context.Background()).MaxDepth(maxDepth).PostCheckPermissionOrErrorBody(postCheckPermissionOrErrorBody).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `PermissionApi.PostCheckPermissionOrError``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `PostCheckPermissionOrError`: CheckPermissionResult + fmt.Fprintf(os.Stdout, "Response from `PermissionApi.PostCheckPermissionOrError`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiPostCheckPermissionOrErrorRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **maxDepth** | **int64** | | + **postCheckPermissionOrErrorBody** | [**PostCheckPermissionOrErrorBody**](PostCheckPermissionOrErrorBody.md) | | + +### Return type + +[**CheckPermissionResult**](CheckPermissionResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + diff --git a/internal/httpclient/docs/PostCheckPermissionBody.md b/internal/httpclient/docs/PostCheckPermissionBody.md new file mode 100644 index 0000000..852cc00 --- /dev/null +++ b/internal/httpclient/docs/PostCheckPermissionBody.md @@ -0,0 +1,160 @@ +# PostCheckPermissionBody + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | Pointer to **string** | Namespace to query | [optional] +**Object** | Pointer to **string** | Object to query | [optional] +**Relation** | Pointer to **string** | Relation to query | [optional] +**SubjectId** | Pointer to **string** | SubjectID to query Either SubjectSet or SubjectID can be provided. | [optional] +**SubjectSet** | Pointer to [**SubjectSet**](SubjectSet.md) | | [optional] + +## Methods + +### NewPostCheckPermissionBody + +`func NewPostCheckPermissionBody() *PostCheckPermissionBody` + +NewPostCheckPermissionBody instantiates a new PostCheckPermissionBody object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewPostCheckPermissionBodyWithDefaults + +`func NewPostCheckPermissionBodyWithDefaults() *PostCheckPermissionBody` + +NewPostCheckPermissionBodyWithDefaults instantiates a new PostCheckPermissionBody object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *PostCheckPermissionBody) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *PostCheckPermissionBody) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *PostCheckPermissionBody) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + +### HasNamespace + +`func (o *PostCheckPermissionBody) HasNamespace() bool` + +HasNamespace returns a boolean if a field has been set. + +### GetObject + +`func (o *PostCheckPermissionBody) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *PostCheckPermissionBody) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *PostCheckPermissionBody) SetObject(v string)` + +SetObject sets Object field to given value. + +### HasObject + +`func (o *PostCheckPermissionBody) HasObject() bool` + +HasObject returns a boolean if a field has been set. + +### GetRelation + +`func (o *PostCheckPermissionBody) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *PostCheckPermissionBody) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *PostCheckPermissionBody) SetRelation(v string)` + +SetRelation sets Relation field to given value. + +### HasRelation + +`func (o *PostCheckPermissionBody) HasRelation() bool` + +HasRelation returns a boolean if a field has been set. + +### GetSubjectId + +`func (o *PostCheckPermissionBody) GetSubjectId() string` + +GetSubjectId returns the SubjectId field if non-nil, zero value otherwise. + +### GetSubjectIdOk + +`func (o *PostCheckPermissionBody) GetSubjectIdOk() (*string, bool)` + +GetSubjectIdOk returns a tuple with the SubjectId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectId + +`func (o *PostCheckPermissionBody) SetSubjectId(v string)` + +SetSubjectId sets SubjectId field to given value. + +### HasSubjectId + +`func (o *PostCheckPermissionBody) HasSubjectId() bool` + +HasSubjectId returns a boolean if a field has been set. + +### GetSubjectSet + +`func (o *PostCheckPermissionBody) GetSubjectSet() SubjectSet` + +GetSubjectSet returns the SubjectSet field if non-nil, zero value otherwise. + +### GetSubjectSetOk + +`func (o *PostCheckPermissionBody) GetSubjectSetOk() (*SubjectSet, bool)` + +GetSubjectSetOk returns a tuple with the SubjectSet field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectSet + +`func (o *PostCheckPermissionBody) SetSubjectSet(v SubjectSet)` + +SetSubjectSet sets SubjectSet field to given value. + +### HasSubjectSet + +`func (o *PostCheckPermissionBody) HasSubjectSet() bool` + +HasSubjectSet returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/PostCheckPermissionOrErrorBody.md b/internal/httpclient/docs/PostCheckPermissionOrErrorBody.md new file mode 100644 index 0000000..821c2a5 --- /dev/null +++ b/internal/httpclient/docs/PostCheckPermissionOrErrorBody.md @@ -0,0 +1,160 @@ +# PostCheckPermissionOrErrorBody + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | Pointer to **string** | Namespace to query | [optional] +**Object** | Pointer to **string** | Object to query | [optional] +**Relation** | Pointer to **string** | Relation to query | [optional] +**SubjectId** | Pointer to **string** | SubjectID to query Either SubjectSet or SubjectID can be provided. | [optional] +**SubjectSet** | Pointer to [**SubjectSet**](SubjectSet.md) | | [optional] + +## Methods + +### NewPostCheckPermissionOrErrorBody + +`func NewPostCheckPermissionOrErrorBody() *PostCheckPermissionOrErrorBody` + +NewPostCheckPermissionOrErrorBody instantiates a new PostCheckPermissionOrErrorBody object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewPostCheckPermissionOrErrorBodyWithDefaults + +`func NewPostCheckPermissionOrErrorBodyWithDefaults() *PostCheckPermissionOrErrorBody` + +NewPostCheckPermissionOrErrorBodyWithDefaults instantiates a new PostCheckPermissionOrErrorBody object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *PostCheckPermissionOrErrorBody) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *PostCheckPermissionOrErrorBody) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *PostCheckPermissionOrErrorBody) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + +### HasNamespace + +`func (o *PostCheckPermissionOrErrorBody) HasNamespace() bool` + +HasNamespace returns a boolean if a field has been set. + +### GetObject + +`func (o *PostCheckPermissionOrErrorBody) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *PostCheckPermissionOrErrorBody) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *PostCheckPermissionOrErrorBody) SetObject(v string)` + +SetObject sets Object field to given value. + +### HasObject + +`func (o *PostCheckPermissionOrErrorBody) HasObject() bool` + +HasObject returns a boolean if a field has been set. + +### GetRelation + +`func (o *PostCheckPermissionOrErrorBody) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *PostCheckPermissionOrErrorBody) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *PostCheckPermissionOrErrorBody) SetRelation(v string)` + +SetRelation sets Relation field to given value. + +### HasRelation + +`func (o *PostCheckPermissionOrErrorBody) HasRelation() bool` + +HasRelation returns a boolean if a field has been set. + +### GetSubjectId + +`func (o *PostCheckPermissionOrErrorBody) GetSubjectId() string` + +GetSubjectId returns the SubjectId field if non-nil, zero value otherwise. + +### GetSubjectIdOk + +`func (o *PostCheckPermissionOrErrorBody) GetSubjectIdOk() (*string, bool)` + +GetSubjectIdOk returns a tuple with the SubjectId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectId + +`func (o *PostCheckPermissionOrErrorBody) SetSubjectId(v string)` + +SetSubjectId sets SubjectId field to given value. + +### HasSubjectId + +`func (o *PostCheckPermissionOrErrorBody) HasSubjectId() bool` + +HasSubjectId returns a boolean if a field has been set. + +### GetSubjectSet + +`func (o *PostCheckPermissionOrErrorBody) GetSubjectSet() SubjectSet` + +GetSubjectSet returns the SubjectSet field if non-nil, zero value otherwise. + +### GetSubjectSetOk + +`func (o *PostCheckPermissionOrErrorBody) GetSubjectSetOk() (*SubjectSet, bool)` + +GetSubjectSetOk returns a tuple with the SubjectSet field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectSet + +`func (o *PostCheckPermissionOrErrorBody) SetSubjectSet(v SubjectSet)` + +SetSubjectSet sets SubjectSet field to given value. + +### HasSubjectSet + +`func (o *PostCheckPermissionOrErrorBody) HasSubjectSet() bool` + +HasSubjectSet returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/RelationQuery.md b/internal/httpclient/docs/RelationQuery.md new file mode 100644 index 0000000..801006e --- /dev/null +++ b/internal/httpclient/docs/RelationQuery.md @@ -0,0 +1,160 @@ +# RelationQuery + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | Pointer to **string** | Namespace to query | [optional] +**Object** | Pointer to **string** | Object to query | [optional] +**Relation** | Pointer to **string** | Relation to query | [optional] +**SubjectId** | Pointer to **string** | SubjectID to query Either SubjectSet or SubjectID can be provided. | [optional] +**SubjectSet** | Pointer to [**SubjectSet**](SubjectSet.md) | | [optional] + +## Methods + +### NewRelationQuery + +`func NewRelationQuery() *RelationQuery` + +NewRelationQuery instantiates a new RelationQuery object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewRelationQueryWithDefaults + +`func NewRelationQueryWithDefaults() *RelationQuery` + +NewRelationQueryWithDefaults instantiates a new RelationQuery object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *RelationQuery) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *RelationQuery) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *RelationQuery) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + +### HasNamespace + +`func (o *RelationQuery) HasNamespace() bool` + +HasNamespace returns a boolean if a field has been set. + +### GetObject + +`func (o *RelationQuery) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *RelationQuery) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *RelationQuery) SetObject(v string)` + +SetObject sets Object field to given value. + +### HasObject + +`func (o *RelationQuery) HasObject() bool` + +HasObject returns a boolean if a field has been set. + +### GetRelation + +`func (o *RelationQuery) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *RelationQuery) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *RelationQuery) SetRelation(v string)` + +SetRelation sets Relation field to given value. + +### HasRelation + +`func (o *RelationQuery) HasRelation() bool` + +HasRelation returns a boolean if a field has been set. + +### GetSubjectId + +`func (o *RelationQuery) GetSubjectId() string` + +GetSubjectId returns the SubjectId field if non-nil, zero value otherwise. + +### GetSubjectIdOk + +`func (o *RelationQuery) GetSubjectIdOk() (*string, bool)` + +GetSubjectIdOk returns a tuple with the SubjectId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectId + +`func (o *RelationQuery) SetSubjectId(v string)` + +SetSubjectId sets SubjectId field to given value. + +### HasSubjectId + +`func (o *RelationQuery) HasSubjectId() bool` + +HasSubjectId returns a boolean if a field has been set. + +### GetSubjectSet + +`func (o *RelationQuery) GetSubjectSet() SubjectSet` + +GetSubjectSet returns the SubjectSet field if non-nil, zero value otherwise. + +### GetSubjectSetOk + +`func (o *RelationQuery) GetSubjectSetOk() (*SubjectSet, bool)` + +GetSubjectSetOk returns a tuple with the SubjectSet field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectSet + +`func (o *RelationQuery) SetSubjectSet(v SubjectSet)` + +SetSubjectSet sets SubjectSet field to given value. + +### HasSubjectSet + +`func (o *RelationQuery) HasSubjectSet() bool` + +HasSubjectSet returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/Relationship.md b/internal/httpclient/docs/Relationship.md new file mode 100644 index 0000000..6ac1304 --- /dev/null +++ b/internal/httpclient/docs/Relationship.md @@ -0,0 +1,145 @@ +# Relationship + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | **string** | Namespace of the Relation Tuple | +**Object** | **string** | Object of the Relation Tuple | +**Relation** | **string** | Relation of the Relation Tuple | +**SubjectId** | Pointer to **string** | SubjectID of the Relation Tuple Either SubjectSet or SubjectID can be provided. | [optional] +**SubjectSet** | Pointer to [**SubjectSet**](SubjectSet.md) | | [optional] + +## Methods + +### NewRelationship + +`func NewRelationship(namespace string, object string, relation string, ) *Relationship` + +NewRelationship instantiates a new Relationship object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewRelationshipWithDefaults + +`func NewRelationshipWithDefaults() *Relationship` + +NewRelationshipWithDefaults instantiates a new Relationship object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *Relationship) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *Relationship) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *Relationship) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + + +### GetObject + +`func (o *Relationship) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *Relationship) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *Relationship) SetObject(v string)` + +SetObject sets Object field to given value. + + +### GetRelation + +`func (o *Relationship) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *Relationship) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *Relationship) SetRelation(v string)` + +SetRelation sets Relation field to given value. + + +### GetSubjectId + +`func (o *Relationship) GetSubjectId() string` + +GetSubjectId returns the SubjectId field if non-nil, zero value otherwise. + +### GetSubjectIdOk + +`func (o *Relationship) GetSubjectIdOk() (*string, bool)` + +GetSubjectIdOk returns a tuple with the SubjectId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectId + +`func (o *Relationship) SetSubjectId(v string)` + +SetSubjectId sets SubjectId field to given value. + +### HasSubjectId + +`func (o *Relationship) HasSubjectId() bool` + +HasSubjectId returns a boolean if a field has been set. + +### GetSubjectSet + +`func (o *Relationship) GetSubjectSet() SubjectSet` + +GetSubjectSet returns the SubjectSet field if non-nil, zero value otherwise. + +### GetSubjectSetOk + +`func (o *Relationship) GetSubjectSetOk() (*SubjectSet, bool)` + +GetSubjectSetOk returns a tuple with the SubjectSet field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSubjectSet + +`func (o *Relationship) SetSubjectSet(v SubjectSet)` + +SetSubjectSet sets SubjectSet field to given value. + +### HasSubjectSet + +`func (o *Relationship) HasSubjectSet() bool` + +HasSubjectSet returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/RelationshipApi.md b/internal/httpclient/docs/RelationshipApi.md new file mode 100644 index 0000000..f7f1516 --- /dev/null +++ b/internal/httpclient/docs/RelationshipApi.md @@ -0,0 +1,429 @@ +# \RelationshipApi + +All URIs are relative to *http://localhost* + +Method | HTTP request | Description +------------- | ------------- | ------------- +[**CheckOplSyntax**](RelationshipApi.md#CheckOplSyntax) | **Post** /opl/syntax/check | Check the syntax of an OPL file +[**CreateRelationship**](RelationshipApi.md#CreateRelationship) | **Put** /admin/relation-tuples | Create a Relationship +[**DeleteRelationships**](RelationshipApi.md#DeleteRelationships) | **Delete** /admin/relation-tuples | Delete Relationships +[**GetRelationships**](RelationshipApi.md#GetRelationships) | **Get** /relation-tuples | Query relationships +[**ListRelationshipNamespaces**](RelationshipApi.md#ListRelationshipNamespaces) | **Get** /namespaces | Query namespaces +[**PatchRelationships**](RelationshipApi.md#PatchRelationships) | **Patch** /admin/relation-tuples | Patch Multiple Relationships + + + +## CheckOplSyntax + +> CheckOplSyntaxResult CheckOplSyntax(ctx).Body(body).Execute() + +Check the syntax of an OPL file + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + body := "body_example" // string | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.CheckOplSyntax(context.Background()).Body(body).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.CheckOplSyntax``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `CheckOplSyntax`: CheckOplSyntaxResult + fmt.Fprintf(os.Stdout, "Response from `RelationshipApi.CheckOplSyntax`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiCheckOplSyntaxRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **body** | **string** | | + +### Return type + +[**CheckOplSyntaxResult**](CheckOplSyntaxResult.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: text/plain +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## CreateRelationship + +> Relationship CreateRelationship(ctx).CreateRelationshipBody(createRelationshipBody).Execute() + +Create a Relationship + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + createRelationshipBody := *openapiclient.NewCreateRelationshipBody() // CreateRelationshipBody | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.CreateRelationship(context.Background()).CreateRelationshipBody(createRelationshipBody).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.CreateRelationship``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `CreateRelationship`: Relationship + fmt.Fprintf(os.Stdout, "Response from `RelationshipApi.CreateRelationship`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiCreateRelationshipRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **createRelationshipBody** | [**CreateRelationshipBody**](CreateRelationshipBody.md) | | + +### Return type + +[**Relationship**](Relationship.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## DeleteRelationships + +> DeleteRelationships(ctx).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).Execute() + +Delete Relationships + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + namespace := "namespace_example" // string | Namespace of the Relationship (optional) + object := "object_example" // string | Object of the Relationship (optional) + relation := "relation_example" // string | Relation of the Relationship (optional) + subjectId := "subjectId_example" // string | SubjectID of the Relationship (optional) + subjectSetNamespace := "subjectSetNamespace_example" // string | Namespace of the Subject Set (optional) + subjectSetObject := "subjectSetObject_example" // string | Object of the Subject Set (optional) + subjectSetRelation := "subjectSetRelation_example" // string | Relation of the Subject Set (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.DeleteRelationships(context.Background()).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.DeleteRelationships``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiDeleteRelationshipsRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **namespace** | **string** | Namespace of the Relationship | + **object** | **string** | Object of the Relationship | + **relation** | **string** | Relation of the Relationship | + **subjectId** | **string** | SubjectID of the Relationship | + **subjectSetNamespace** | **string** | Namespace of the Subject Set | + **subjectSetObject** | **string** | Object of the Subject Set | + **subjectSetRelation** | **string** | Relation of the Subject Set | + +### Return type + + (empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## GetRelationships + +> Relationships GetRelationships(ctx).PageSize(pageSize).PageToken(pageToken).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).Execute() + +Query relationships + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + pageSize := int64(789) // int64 | Items per Page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). (optional) (default to 250) + pageToken := "pageToken_example" // string | Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). (optional) + namespace := "namespace_example" // string | Namespace of the Relationship (optional) + object := "object_example" // string | Object of the Relationship (optional) + relation := "relation_example" // string | Relation of the Relationship (optional) + subjectId := "subjectId_example" // string | SubjectID of the Relationship (optional) + subjectSetNamespace := "subjectSetNamespace_example" // string | Namespace of the Subject Set (optional) + subjectSetObject := "subjectSetObject_example" // string | Object of the Subject Set (optional) + subjectSetRelation := "subjectSetRelation_example" // string | Relation of the Subject Set (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.GetRelationships(context.Background()).PageSize(pageSize).PageToken(pageToken).Namespace(namespace).Object(object).Relation(relation).SubjectId(subjectId).SubjectSetNamespace(subjectSetNamespace).SubjectSetObject(subjectSetObject).SubjectSetRelation(subjectSetRelation).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.GetRelationships``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `GetRelationships`: Relationships + fmt.Fprintf(os.Stdout, "Response from `RelationshipApi.GetRelationships`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiGetRelationshipsRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **pageSize** | **int64** | Items per Page This is the number of items per page to return. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). | [default to 250] + **pageToken** | **string** | Next Page Token The next page token. For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). | + **namespace** | **string** | Namespace of the Relationship | + **object** | **string** | Object of the Relationship | + **relation** | **string** | Relation of the Relationship | + **subjectId** | **string** | SubjectID of the Relationship | + **subjectSetNamespace** | **string** | Namespace of the Subject Set | + **subjectSetObject** | **string** | Object of the Subject Set | + **subjectSetRelation** | **string** | Relation of the Subject Set | + +### Return type + +[**Relationships**](Relationships.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## ListRelationshipNamespaces + +> RelationshipNamespaces ListRelationshipNamespaces(ctx).Execute() + +Query namespaces + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.ListRelationshipNamespaces(context.Background()).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.ListRelationshipNamespaces``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `ListRelationshipNamespaces`: RelationshipNamespaces + fmt.Fprintf(os.Stdout, "Response from `RelationshipApi.ListRelationshipNamespaces`: %v\n", resp) +} +``` + +### Path Parameters + +This endpoint does not need any parameter. + +### Other Parameters + +Other parameters are passed through a pointer to a apiListRelationshipNamespacesRequest struct via the builder pattern + + +### Return type + +[**RelationshipNamespaces**](RelationshipNamespaces.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + +## PatchRelationships + +> PatchRelationships(ctx).RelationshipPatch(relationshipPatch).Execute() + +Patch Multiple Relationships + + + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "./openapi" +) + +func main() { + relationshipPatch := []openapiclient.RelationshipPatch{*openapiclient.NewRelationshipPatch()} // []RelationshipPatch | (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.RelationshipApi.PatchRelationships(context.Background()).RelationshipPatch(relationshipPatch).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `RelationshipApi.PatchRelationships``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiPatchRelationshipsRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **relationshipPatch** | [**[]RelationshipPatch**](RelationshipPatch.md) | | + +### Return type + + (empty response body) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + diff --git a/internal/httpclient/docs/RelationshipNamespaces.md b/internal/httpclient/docs/RelationshipNamespaces.md new file mode 100644 index 0000000..63fa287 --- /dev/null +++ b/internal/httpclient/docs/RelationshipNamespaces.md @@ -0,0 +1,56 @@ +# RelationshipNamespaces + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespaces** | Pointer to [**[]Namespace**](Namespace.md) | | [optional] + +## Methods + +### NewRelationshipNamespaces + +`func NewRelationshipNamespaces() *RelationshipNamespaces` + +NewRelationshipNamespaces instantiates a new RelationshipNamespaces object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewRelationshipNamespacesWithDefaults + +`func NewRelationshipNamespacesWithDefaults() *RelationshipNamespaces` + +NewRelationshipNamespacesWithDefaults instantiates a new RelationshipNamespaces object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespaces + +`func (o *RelationshipNamespaces) GetNamespaces() []Namespace` + +GetNamespaces returns the Namespaces field if non-nil, zero value otherwise. + +### GetNamespacesOk + +`func (o *RelationshipNamespaces) GetNamespacesOk() (*[]Namespace, bool)` + +GetNamespacesOk returns a tuple with the Namespaces field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespaces + +`func (o *RelationshipNamespaces) SetNamespaces(v []Namespace)` + +SetNamespaces sets Namespaces field to given value. + +### HasNamespaces + +`func (o *RelationshipNamespaces) HasNamespaces() bool` + +HasNamespaces returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/RelationshipPatch.md b/internal/httpclient/docs/RelationshipPatch.md new file mode 100644 index 0000000..649c625 --- /dev/null +++ b/internal/httpclient/docs/RelationshipPatch.md @@ -0,0 +1,82 @@ +# RelationshipPatch + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Action** | Pointer to **string** | | [optional] +**RelationTuple** | Pointer to [**Relationship**](Relationship.md) | | [optional] + +## Methods + +### NewRelationshipPatch + +`func NewRelationshipPatch() *RelationshipPatch` + +NewRelationshipPatch instantiates a new RelationshipPatch object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewRelationshipPatchWithDefaults + +`func NewRelationshipPatchWithDefaults() *RelationshipPatch` + +NewRelationshipPatchWithDefaults instantiates a new RelationshipPatch object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetAction + +`func (o *RelationshipPatch) GetAction() string` + +GetAction returns the Action field if non-nil, zero value otherwise. + +### GetActionOk + +`func (o *RelationshipPatch) GetActionOk() (*string, bool)` + +GetActionOk returns a tuple with the Action field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetAction + +`func (o *RelationshipPatch) SetAction(v string)` + +SetAction sets Action field to given value. + +### HasAction + +`func (o *RelationshipPatch) HasAction() bool` + +HasAction returns a boolean if a field has been set. + +### GetRelationTuple + +`func (o *RelationshipPatch) GetRelationTuple() Relationship` + +GetRelationTuple returns the RelationTuple field if non-nil, zero value otherwise. + +### GetRelationTupleOk + +`func (o *RelationshipPatch) GetRelationTupleOk() (*Relationship, bool)` + +GetRelationTupleOk returns a tuple with the RelationTuple field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelationTuple + +`func (o *RelationshipPatch) SetRelationTuple(v Relationship)` + +SetRelationTuple sets RelationTuple field to given value. + +### HasRelationTuple + +`func (o *RelationshipPatch) HasRelationTuple() bool` + +HasRelationTuple returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/Relationships.md b/internal/httpclient/docs/Relationships.md new file mode 100644 index 0000000..83f813e --- /dev/null +++ b/internal/httpclient/docs/Relationships.md @@ -0,0 +1,82 @@ +# Relationships + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**NextPageToken** | Pointer to **string** | The opaque token to provide in a subsequent request to get the next page. It is the empty string iff this is the last page. | [optional] +**RelationTuples** | Pointer to [**[]Relationship**](Relationship.md) | | [optional] + +## Methods + +### NewRelationships + +`func NewRelationships() *Relationships` + +NewRelationships instantiates a new Relationships object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewRelationshipsWithDefaults + +`func NewRelationshipsWithDefaults() *Relationships` + +NewRelationshipsWithDefaults instantiates a new Relationships object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNextPageToken + +`func (o *Relationships) GetNextPageToken() string` + +GetNextPageToken returns the NextPageToken field if non-nil, zero value otherwise. + +### GetNextPageTokenOk + +`func (o *Relationships) GetNextPageTokenOk() (*string, bool)` + +GetNextPageTokenOk returns a tuple with the NextPageToken field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNextPageToken + +`func (o *Relationships) SetNextPageToken(v string)` + +SetNextPageToken sets NextPageToken field to given value. + +### HasNextPageToken + +`func (o *Relationships) HasNextPageToken() bool` + +HasNextPageToken returns a boolean if a field has been set. + +### GetRelationTuples + +`func (o *Relationships) GetRelationTuples() []Relationship` + +GetRelationTuples returns the RelationTuples field if non-nil, zero value otherwise. + +### GetRelationTuplesOk + +`func (o *Relationships) GetRelationTuplesOk() (*[]Relationship, bool)` + +GetRelationTuplesOk returns a tuple with the RelationTuples field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelationTuples + +`func (o *Relationships) SetRelationTuples(v []Relationship)` + +SetRelationTuples sets RelationTuples field to given value. + +### HasRelationTuples + +`func (o *Relationships) HasRelationTuples() bool` + +HasRelationTuples returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/SourcePosition.md b/internal/httpclient/docs/SourcePosition.md new file mode 100644 index 0000000..000e5be --- /dev/null +++ b/internal/httpclient/docs/SourcePosition.md @@ -0,0 +1,82 @@ +# SourcePosition + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Line** | Pointer to **int64** | | [optional] +**Column** | Pointer to **int64** | | [optional] + +## Methods + +### NewSourcePosition + +`func NewSourcePosition() *SourcePosition` + +NewSourcePosition instantiates a new SourcePosition object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewSourcePositionWithDefaults + +`func NewSourcePositionWithDefaults() *SourcePosition` + +NewSourcePositionWithDefaults instantiates a new SourcePosition object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetLine + +`func (o *SourcePosition) GetLine() int64` + +GetLine returns the Line field if non-nil, zero value otherwise. + +### GetLineOk + +`func (o *SourcePosition) GetLineOk() (*int64, bool)` + +GetLineOk returns a tuple with the Line field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetLine + +`func (o *SourcePosition) SetLine(v int64)` + +SetLine sets Line field to given value. + +### HasLine + +`func (o *SourcePosition) HasLine() bool` + +HasLine returns a boolean if a field has been set. + +### GetColumn + +`func (o *SourcePosition) GetColumn() int64` + +GetColumn returns the Column field if non-nil, zero value otherwise. + +### GetColumnOk + +`func (o *SourcePosition) GetColumnOk() (*int64, bool)` + +GetColumnOk returns a tuple with the Column field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetColumn + +`func (o *SourcePosition) SetColumn(v int64)` + +SetColumn sets Column field to given value. + +### HasColumn + +`func (o *SourcePosition) HasColumn() bool` + +HasColumn returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/SubjectSet.md b/internal/httpclient/docs/SubjectSet.md new file mode 100644 index 0000000..194228e --- /dev/null +++ b/internal/httpclient/docs/SubjectSet.md @@ -0,0 +1,93 @@ +# SubjectSet + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Namespace** | **string** | Namespace of the Subject Set | +**Object** | **string** | Object of the Subject Set | +**Relation** | **string** | Relation of the Subject Set | + +## Methods + +### NewSubjectSet + +`func NewSubjectSet(namespace string, object string, relation string, ) *SubjectSet` + +NewSubjectSet instantiates a new SubjectSet object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewSubjectSetWithDefaults + +`func NewSubjectSetWithDefaults() *SubjectSet` + +NewSubjectSetWithDefaults instantiates a new SubjectSet object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetNamespace + +`func (o *SubjectSet) GetNamespace() string` + +GetNamespace returns the Namespace field if non-nil, zero value otherwise. + +### GetNamespaceOk + +`func (o *SubjectSet) GetNamespaceOk() (*string, bool)` + +GetNamespaceOk returns a tuple with the Namespace field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetNamespace + +`func (o *SubjectSet) SetNamespace(v string)` + +SetNamespace sets Namespace field to given value. + + +### GetObject + +`func (o *SubjectSet) GetObject() string` + +GetObject returns the Object field if non-nil, zero value otherwise. + +### GetObjectOk + +`func (o *SubjectSet) GetObjectOk() (*string, bool)` + +GetObjectOk returns a tuple with the Object field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetObject + +`func (o *SubjectSet) SetObject(v string)` + +SetObject sets Object field to given value. + + +### GetRelation + +`func (o *SubjectSet) GetRelation() string` + +GetRelation returns the Relation field if non-nil, zero value otherwise. + +### GetRelationOk + +`func (o *SubjectSet) GetRelationOk() (*string, bool)` + +GetRelationOk returns a tuple with the Relation field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetRelation + +`func (o *SubjectSet) SetRelation(v string)` + +SetRelation sets Relation field to given value. + + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/docs/Version.md b/internal/httpclient/docs/Version.md new file mode 100644 index 0000000..c186956 --- /dev/null +++ b/internal/httpclient/docs/Version.md @@ -0,0 +1,56 @@ +# Version + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Version** | Pointer to **string** | Version is the service's version. | [optional] + +## Methods + +### NewVersion + +`func NewVersion() *Version` + +NewVersion instantiates a new Version object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewVersionWithDefaults + +`func NewVersionWithDefaults() *Version` + +NewVersionWithDefaults instantiates a new Version object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetVersion + +`func (o *Version) GetVersion() string` + +GetVersion returns the Version field if non-nil, zero value otherwise. + +### GetVersionOk + +`func (o *Version) GetVersionOk() (*string, bool)` + +GetVersionOk returns a tuple with the Version field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetVersion + +`func (o *Version) SetVersion(v string)` + +SetVersion sets Version field to given value. + +### HasVersion + +`func (o *Version) HasVersion() bool` + +HasVersion returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/internal/httpclient/git_push.sh b/internal/httpclient/git_push.sh new file mode 100644 index 0000000..18f76bf --- /dev/null +++ b/internal/httpclient/git_push.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="ory" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="keto-client-go" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/internal/httpclient/model_batch_check_permission_body.go b/internal/httpclient/model_batch_check_permission_body.go new file mode 100644 index 0000000..e6e25ab --- /dev/null +++ b/internal/httpclient/model_batch_check_permission_body.go @@ -0,0 +1,114 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// BatchCheckPermissionBody Batch Check Permission Body +type BatchCheckPermissionBody struct { + Tuples []Relationship `json:"tuples,omitempty"` +} + +// NewBatchCheckPermissionBody instantiates a new BatchCheckPermissionBody object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewBatchCheckPermissionBody() *BatchCheckPermissionBody { + this := BatchCheckPermissionBody{} + return &this +} + +// NewBatchCheckPermissionBodyWithDefaults instantiates a new BatchCheckPermissionBody object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewBatchCheckPermissionBodyWithDefaults() *BatchCheckPermissionBody { + this := BatchCheckPermissionBody{} + return &this +} + +// GetTuples returns the Tuples field value if set, zero value otherwise. +func (o *BatchCheckPermissionBody) GetTuples() []Relationship { + if o == nil || o.Tuples == nil { + var ret []Relationship + return ret + } + return o.Tuples +} + +// GetTuplesOk returns a tuple with the Tuples field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BatchCheckPermissionBody) GetTuplesOk() ([]Relationship, bool) { + if o == nil || o.Tuples == nil { + return nil, false + } + return o.Tuples, true +} + +// HasTuples returns a boolean if a field has been set. +func (o *BatchCheckPermissionBody) HasTuples() bool { + if o != nil && o.Tuples != nil { + return true + } + + return false +} + +// SetTuples gets a reference to the given []Relationship and assigns it to the Tuples field. +func (o *BatchCheckPermissionBody) SetTuples(v []Relationship) { + o.Tuples = v +} + +func (o BatchCheckPermissionBody) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Tuples != nil { + toSerialize["tuples"] = o.Tuples + } + return json.Marshal(toSerialize) +} + +type NullableBatchCheckPermissionBody struct { + value *BatchCheckPermissionBody + isSet bool +} + +func (v NullableBatchCheckPermissionBody) Get() *BatchCheckPermissionBody { + return v.value +} + +func (v *NullableBatchCheckPermissionBody) Set(val *BatchCheckPermissionBody) { + v.value = val + v.isSet = true +} + +func (v NullableBatchCheckPermissionBody) IsSet() bool { + return v.isSet +} + +func (v *NullableBatchCheckPermissionBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBatchCheckPermissionBody(val *BatchCheckPermissionBody) *NullableBatchCheckPermissionBody { + return &NullableBatchCheckPermissionBody{value: val, isSet: true} +} + +func (v NullableBatchCheckPermissionBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBatchCheckPermissionBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_batch_check_permission_result.go b/internal/httpclient/model_batch_check_permission_result.go new file mode 100644 index 0000000..8b88cfa --- /dev/null +++ b/internal/httpclient/model_batch_check_permission_result.go @@ -0,0 +1,108 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// BatchCheckPermissionResult Batch Check Permission Result +type BatchCheckPermissionResult struct { + // An array of check results. The order aligns with the input order. + Results []CheckPermissionResultWithError `json:"results"` +} + +// NewBatchCheckPermissionResult instantiates a new BatchCheckPermissionResult object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewBatchCheckPermissionResult(results []CheckPermissionResultWithError) *BatchCheckPermissionResult { + this := BatchCheckPermissionResult{} + this.Results = results + return &this +} + +// NewBatchCheckPermissionResultWithDefaults instantiates a new BatchCheckPermissionResult object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewBatchCheckPermissionResultWithDefaults() *BatchCheckPermissionResult { + this := BatchCheckPermissionResult{} + return &this +} + +// GetResults returns the Results field value +func (o *BatchCheckPermissionResult) GetResults() []CheckPermissionResultWithError { + if o == nil { + var ret []CheckPermissionResultWithError + return ret + } + + return o.Results +} + +// GetResultsOk returns a tuple with the Results field value +// and a boolean to check if the value has been set. +func (o *BatchCheckPermissionResult) GetResultsOk() ([]CheckPermissionResultWithError, bool) { + if o == nil { + return nil, false + } + return o.Results, true +} + +// SetResults sets field value +func (o *BatchCheckPermissionResult) SetResults(v []CheckPermissionResultWithError) { + o.Results = v +} + +func (o BatchCheckPermissionResult) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["results"] = o.Results + } + return json.Marshal(toSerialize) +} + +type NullableBatchCheckPermissionResult struct { + value *BatchCheckPermissionResult + isSet bool +} + +func (v NullableBatchCheckPermissionResult) Get() *BatchCheckPermissionResult { + return v.value +} + +func (v *NullableBatchCheckPermissionResult) Set(val *BatchCheckPermissionResult) { + v.value = val + v.isSet = true +} + +func (v NullableBatchCheckPermissionResult) IsSet() bool { + return v.isSet +} + +func (v *NullableBatchCheckPermissionResult) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBatchCheckPermissionResult(val *BatchCheckPermissionResult) *NullableBatchCheckPermissionResult { + return &NullableBatchCheckPermissionResult{value: val, isSet: true} +} + +func (v NullableBatchCheckPermissionResult) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBatchCheckPermissionResult) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_check_opl_syntax_result.go b/internal/httpclient/model_check_opl_syntax_result.go new file mode 100644 index 0000000..0a117cd --- /dev/null +++ b/internal/httpclient/model_check_opl_syntax_result.go @@ -0,0 +1,115 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// CheckOplSyntaxResult struct for CheckOplSyntaxResult +type CheckOplSyntaxResult struct { + // The list of syntax errors + Errors []ParseError `json:"errors,omitempty"` +} + +// NewCheckOplSyntaxResult instantiates a new CheckOplSyntaxResult object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCheckOplSyntaxResult() *CheckOplSyntaxResult { + this := CheckOplSyntaxResult{} + return &this +} + +// NewCheckOplSyntaxResultWithDefaults instantiates a new CheckOplSyntaxResult object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCheckOplSyntaxResultWithDefaults() *CheckOplSyntaxResult { + this := CheckOplSyntaxResult{} + return &this +} + +// GetErrors returns the Errors field value if set, zero value otherwise. +func (o *CheckOplSyntaxResult) GetErrors() []ParseError { + if o == nil || o.Errors == nil { + var ret []ParseError + return ret + } + return o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CheckOplSyntaxResult) GetErrorsOk() ([]ParseError, bool) { + if o == nil || o.Errors == nil { + return nil, false + } + return o.Errors, true +} + +// HasErrors returns a boolean if a field has been set. +func (o *CheckOplSyntaxResult) HasErrors() bool { + if o != nil && o.Errors != nil { + return true + } + + return false +} + +// SetErrors gets a reference to the given []ParseError and assigns it to the Errors field. +func (o *CheckOplSyntaxResult) SetErrors(v []ParseError) { + o.Errors = v +} + +func (o CheckOplSyntaxResult) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Errors != nil { + toSerialize["errors"] = o.Errors + } + return json.Marshal(toSerialize) +} + +type NullableCheckOplSyntaxResult struct { + value *CheckOplSyntaxResult + isSet bool +} + +func (v NullableCheckOplSyntaxResult) Get() *CheckOplSyntaxResult { + return v.value +} + +func (v *NullableCheckOplSyntaxResult) Set(val *CheckOplSyntaxResult) { + v.value = val + v.isSet = true +} + +func (v NullableCheckOplSyntaxResult) IsSet() bool { + return v.isSet +} + +func (v *NullableCheckOplSyntaxResult) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCheckOplSyntaxResult(val *CheckOplSyntaxResult) *NullableCheckOplSyntaxResult { + return &NullableCheckOplSyntaxResult{value: val, isSet: true} +} + +func (v NullableCheckOplSyntaxResult) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCheckOplSyntaxResult) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_check_permission_result.go b/internal/httpclient/model_check_permission_result.go new file mode 100644 index 0000000..267290d --- /dev/null +++ b/internal/httpclient/model_check_permission_result.go @@ -0,0 +1,108 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// CheckPermissionResult The content of the allowed field is mirrored in the HTTP status code. +type CheckPermissionResult struct { + // whether the relation tuple is allowed + Allowed bool `json:"allowed"` +} + +// NewCheckPermissionResult instantiates a new CheckPermissionResult object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCheckPermissionResult(allowed bool) *CheckPermissionResult { + this := CheckPermissionResult{} + this.Allowed = allowed + return &this +} + +// NewCheckPermissionResultWithDefaults instantiates a new CheckPermissionResult object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCheckPermissionResultWithDefaults() *CheckPermissionResult { + this := CheckPermissionResult{} + return &this +} + +// GetAllowed returns the Allowed field value +func (o *CheckPermissionResult) GetAllowed() bool { + if o == nil { + var ret bool + return ret + } + + return o.Allowed +} + +// GetAllowedOk returns a tuple with the Allowed field value +// and a boolean to check if the value has been set. +func (o *CheckPermissionResult) GetAllowedOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Allowed, true +} + +// SetAllowed sets field value +func (o *CheckPermissionResult) SetAllowed(v bool) { + o.Allowed = v +} + +func (o CheckPermissionResult) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["allowed"] = o.Allowed + } + return json.Marshal(toSerialize) +} + +type NullableCheckPermissionResult struct { + value *CheckPermissionResult + isSet bool +} + +func (v NullableCheckPermissionResult) Get() *CheckPermissionResult { + return v.value +} + +func (v *NullableCheckPermissionResult) Set(val *CheckPermissionResult) { + v.value = val + v.isSet = true +} + +func (v NullableCheckPermissionResult) IsSet() bool { + return v.isSet +} + +func (v *NullableCheckPermissionResult) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCheckPermissionResult(val *CheckPermissionResult) *NullableCheckPermissionResult { + return &NullableCheckPermissionResult{value: val, isSet: true} +} + +func (v NullableCheckPermissionResult) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCheckPermissionResult) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_check_permission_result_with_error.go b/internal/httpclient/model_check_permission_result_with_error.go new file mode 100644 index 0000000..92558fa --- /dev/null +++ b/internal/httpclient/model_check_permission_result_with_error.go @@ -0,0 +1,145 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// CheckPermissionResultWithError Check Permission Result With Error +type CheckPermissionResultWithError struct { + // whether the relation tuple is allowed + Allowed bool `json:"allowed"` + // any error generated while checking the relation tuple + Error *string `json:"error,omitempty"` +} + +// NewCheckPermissionResultWithError instantiates a new CheckPermissionResultWithError object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCheckPermissionResultWithError(allowed bool) *CheckPermissionResultWithError { + this := CheckPermissionResultWithError{} + this.Allowed = allowed + return &this +} + +// NewCheckPermissionResultWithErrorWithDefaults instantiates a new CheckPermissionResultWithError object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCheckPermissionResultWithErrorWithDefaults() *CheckPermissionResultWithError { + this := CheckPermissionResultWithError{} + return &this +} + +// GetAllowed returns the Allowed field value +func (o *CheckPermissionResultWithError) GetAllowed() bool { + if o == nil { + var ret bool + return ret + } + + return o.Allowed +} + +// GetAllowedOk returns a tuple with the Allowed field value +// and a boolean to check if the value has been set. +func (o *CheckPermissionResultWithError) GetAllowedOk() (*bool, bool) { + if o == nil { + return nil, false + } + return &o.Allowed, true +} + +// SetAllowed sets field value +func (o *CheckPermissionResultWithError) SetAllowed(v bool) { + o.Allowed = v +} + +// GetError returns the Error field value if set, zero value otherwise. +func (o *CheckPermissionResultWithError) GetError() string { + if o == nil || o.Error == nil { + var ret string + return ret + } + return *o.Error +} + +// GetErrorOk returns a tuple with the Error field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CheckPermissionResultWithError) GetErrorOk() (*string, bool) { + if o == nil || o.Error == nil { + return nil, false + } + return o.Error, true +} + +// HasError returns a boolean if a field has been set. +func (o *CheckPermissionResultWithError) HasError() bool { + if o != nil && o.Error != nil { + return true + } + + return false +} + +// SetError gets a reference to the given string and assigns it to the Error field. +func (o *CheckPermissionResultWithError) SetError(v string) { + o.Error = &v +} + +func (o CheckPermissionResultWithError) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["allowed"] = o.Allowed + } + if o.Error != nil { + toSerialize["error"] = o.Error + } + return json.Marshal(toSerialize) +} + +type NullableCheckPermissionResultWithError struct { + value *CheckPermissionResultWithError + isSet bool +} + +func (v NullableCheckPermissionResultWithError) Get() *CheckPermissionResultWithError { + return v.value +} + +func (v *NullableCheckPermissionResultWithError) Set(val *CheckPermissionResultWithError) { + v.value = val + v.isSet = true +} + +func (v NullableCheckPermissionResultWithError) IsSet() bool { + return v.isSet +} + +func (v *NullableCheckPermissionResultWithError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCheckPermissionResultWithError(val *CheckPermissionResultWithError) *NullableCheckPermissionResultWithError { + return &NullableCheckPermissionResultWithError{value: val, isSet: true} +} + +func (v NullableCheckPermissionResultWithError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCheckPermissionResultWithError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_create_relationship_body.go b/internal/httpclient/model_create_relationship_body.go new file mode 100644 index 0000000..c1c9545 --- /dev/null +++ b/internal/httpclient/model_create_relationship_body.go @@ -0,0 +1,262 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// CreateRelationshipBody Create Relationship Request Body +type CreateRelationshipBody struct { + // Namespace to query + Namespace *string `json:"namespace,omitempty"` + // Object to query + Object *string `json:"object,omitempty"` + // Relation to query + Relation *string `json:"relation,omitempty"` + // SubjectID to query Either SubjectSet or SubjectID can be provided. + SubjectId *string `json:"subject_id,omitempty"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// NewCreateRelationshipBody instantiates a new CreateRelationshipBody object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewCreateRelationshipBody() *CreateRelationshipBody { + this := CreateRelationshipBody{} + return &this +} + +// NewCreateRelationshipBodyWithDefaults instantiates a new CreateRelationshipBody object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewCreateRelationshipBodyWithDefaults() *CreateRelationshipBody { + this := CreateRelationshipBody{} + return &this +} + +// GetNamespace returns the Namespace field value if set, zero value otherwise. +func (o *CreateRelationshipBody) GetNamespace() string { + if o == nil || o.Namespace == nil { + var ret string + return ret + } + return *o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRelationshipBody) GetNamespaceOk() (*string, bool) { + if o == nil || o.Namespace == nil { + return nil, false + } + return o.Namespace, true +} + +// HasNamespace returns a boolean if a field has been set. +func (o *CreateRelationshipBody) HasNamespace() bool { + if o != nil && o.Namespace != nil { + return true + } + + return false +} + +// SetNamespace gets a reference to the given string and assigns it to the Namespace field. +func (o *CreateRelationshipBody) SetNamespace(v string) { + o.Namespace = &v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *CreateRelationshipBody) GetObject() string { + if o == nil || o.Object == nil { + var ret string + return ret + } + return *o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRelationshipBody) GetObjectOk() (*string, bool) { + if o == nil || o.Object == nil { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *CreateRelationshipBody) HasObject() bool { + if o != nil && o.Object != nil { + return true + } + + return false +} + +// SetObject gets a reference to the given string and assigns it to the Object field. +func (o *CreateRelationshipBody) SetObject(v string) { + o.Object = &v +} + +// GetRelation returns the Relation field value if set, zero value otherwise. +func (o *CreateRelationshipBody) GetRelation() string { + if o == nil || o.Relation == nil { + var ret string + return ret + } + return *o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRelationshipBody) GetRelationOk() (*string, bool) { + if o == nil || o.Relation == nil { + return nil, false + } + return o.Relation, true +} + +// HasRelation returns a boolean if a field has been set. +func (o *CreateRelationshipBody) HasRelation() bool { + if o != nil && o.Relation != nil { + return true + } + + return false +} + +// SetRelation gets a reference to the given string and assigns it to the Relation field. +func (o *CreateRelationshipBody) SetRelation(v string) { + o.Relation = &v +} + +// GetSubjectId returns the SubjectId field value if set, zero value otherwise. +func (o *CreateRelationshipBody) GetSubjectId() string { + if o == nil || o.SubjectId == nil { + var ret string + return ret + } + return *o.SubjectId +} + +// GetSubjectIdOk returns a tuple with the SubjectId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRelationshipBody) GetSubjectIdOk() (*string, bool) { + if o == nil || o.SubjectId == nil { + return nil, false + } + return o.SubjectId, true +} + +// HasSubjectId returns a boolean if a field has been set. +func (o *CreateRelationshipBody) HasSubjectId() bool { + if o != nil && o.SubjectId != nil { + return true + } + + return false +} + +// SetSubjectId gets a reference to the given string and assigns it to the SubjectId field. +func (o *CreateRelationshipBody) SetSubjectId(v string) { + o.SubjectId = &v +} + +// GetSubjectSet returns the SubjectSet field value if set, zero value otherwise. +func (o *CreateRelationshipBody) GetSubjectSet() SubjectSet { + if o == nil || o.SubjectSet == nil { + var ret SubjectSet + return ret + } + return *o.SubjectSet +} + +// GetSubjectSetOk returns a tuple with the SubjectSet field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *CreateRelationshipBody) GetSubjectSetOk() (*SubjectSet, bool) { + if o == nil || o.SubjectSet == nil { + return nil, false + } + return o.SubjectSet, true +} + +// HasSubjectSet returns a boolean if a field has been set. +func (o *CreateRelationshipBody) HasSubjectSet() bool { + if o != nil && o.SubjectSet != nil { + return true + } + + return false +} + +// SetSubjectSet gets a reference to the given SubjectSet and assigns it to the SubjectSet field. +func (o *CreateRelationshipBody) SetSubjectSet(v SubjectSet) { + o.SubjectSet = &v +} + +func (o CreateRelationshipBody) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Namespace != nil { + toSerialize["namespace"] = o.Namespace + } + if o.Object != nil { + toSerialize["object"] = o.Object + } + if o.Relation != nil { + toSerialize["relation"] = o.Relation + } + if o.SubjectId != nil { + toSerialize["subject_id"] = o.SubjectId + } + if o.SubjectSet != nil { + toSerialize["subject_set"] = o.SubjectSet + } + return json.Marshal(toSerialize) +} + +type NullableCreateRelationshipBody struct { + value *CreateRelationshipBody + isSet bool +} + +func (v NullableCreateRelationshipBody) Get() *CreateRelationshipBody { + return v.value +} + +func (v *NullableCreateRelationshipBody) Set(val *CreateRelationshipBody) { + v.value = val + v.isSet = true +} + +func (v NullableCreateRelationshipBody) IsSet() bool { + return v.isSet +} + +func (v *NullableCreateRelationshipBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableCreateRelationshipBody(val *CreateRelationshipBody) *NullableCreateRelationshipBody { + return &NullableCreateRelationshipBody{value: val, isSet: true} +} + +func (v NullableCreateRelationshipBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableCreateRelationshipBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_error_generic.go b/internal/httpclient/model_error_generic.go new file mode 100644 index 0000000..8982499 --- /dev/null +++ b/internal/httpclient/model_error_generic.go @@ -0,0 +1,107 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ErrorGeneric The standard Ory JSON API error format. +type ErrorGeneric struct { + Error GenericError `json:"error"` +} + +// NewErrorGeneric instantiates a new ErrorGeneric object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewErrorGeneric(error_ GenericError) *ErrorGeneric { + this := ErrorGeneric{} + this.Error = error_ + return &this +} + +// NewErrorGenericWithDefaults instantiates a new ErrorGeneric object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewErrorGenericWithDefaults() *ErrorGeneric { + this := ErrorGeneric{} + return &this +} + +// GetError returns the Error field value +func (o *ErrorGeneric) GetError() GenericError { + if o == nil { + var ret GenericError + return ret + } + + return o.Error +} + +// GetErrorOk returns a tuple with the Error field value +// and a boolean to check if the value has been set. +func (o *ErrorGeneric) GetErrorOk() (*GenericError, bool) { + if o == nil { + return nil, false + } + return &o.Error, true +} + +// SetError sets field value +func (o *ErrorGeneric) SetError(v GenericError) { + o.Error = v +} + +func (o ErrorGeneric) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["error"] = o.Error + } + return json.Marshal(toSerialize) +} + +type NullableErrorGeneric struct { + value *ErrorGeneric + isSet bool +} + +func (v NullableErrorGeneric) Get() *ErrorGeneric { + return v.value +} + +func (v *NullableErrorGeneric) Set(val *ErrorGeneric) { + v.value = val + v.isSet = true +} + +func (v NullableErrorGeneric) IsSet() bool { + return v.isSet +} + +func (v *NullableErrorGeneric) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableErrorGeneric(val *ErrorGeneric) *NullableErrorGeneric { + return &NullableErrorGeneric{value: val, isSet: true} +} + +func (v NullableErrorGeneric) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableErrorGeneric) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_expanded_permission_tree.go b/internal/httpclient/model_expanded_permission_tree.go new file mode 100644 index 0000000..175f77e --- /dev/null +++ b/internal/httpclient/model_expanded_permission_tree.go @@ -0,0 +1,181 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ExpandedPermissionTree struct for ExpandedPermissionTree +type ExpandedPermissionTree struct { + // The children of the node, possibly none. + Children []ExpandedPermissionTree `json:"children,omitempty"` + Tuple *Relationship `json:"tuple,omitempty"` + // The type of the node. union TreeNodeUnion exclusion TreeNodeExclusion intersection TreeNodeIntersection leaf TreeNodeLeaf tuple_to_subject_set TreeNodeTupleToSubjectSet computed_subject_set TreeNodeComputedSubjectSet not TreeNodeNot unspecified TreeNodeUnspecified + Type string `json:"type"` +} + +// NewExpandedPermissionTree instantiates a new ExpandedPermissionTree object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewExpandedPermissionTree(type_ string) *ExpandedPermissionTree { + this := ExpandedPermissionTree{} + this.Type = type_ + return &this +} + +// NewExpandedPermissionTreeWithDefaults instantiates a new ExpandedPermissionTree object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewExpandedPermissionTreeWithDefaults() *ExpandedPermissionTree { + this := ExpandedPermissionTree{} + return &this +} + +// GetChildren returns the Children field value if set, zero value otherwise. +func (o *ExpandedPermissionTree) GetChildren() []ExpandedPermissionTree { + if o == nil || o.Children == nil { + var ret []ExpandedPermissionTree + return ret + } + return o.Children +} + +// GetChildrenOk returns a tuple with the Children field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ExpandedPermissionTree) GetChildrenOk() ([]ExpandedPermissionTree, bool) { + if o == nil || o.Children == nil { + return nil, false + } + return o.Children, true +} + +// HasChildren returns a boolean if a field has been set. +func (o *ExpandedPermissionTree) HasChildren() bool { + if o != nil && o.Children != nil { + return true + } + + return false +} + +// SetChildren gets a reference to the given []ExpandedPermissionTree and assigns it to the Children field. +func (o *ExpandedPermissionTree) SetChildren(v []ExpandedPermissionTree) { + o.Children = v +} + +// GetTuple returns the Tuple field value if set, zero value otherwise. +func (o *ExpandedPermissionTree) GetTuple() Relationship { + if o == nil || o.Tuple == nil { + var ret Relationship + return ret + } + return *o.Tuple +} + +// GetTupleOk returns a tuple with the Tuple field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ExpandedPermissionTree) GetTupleOk() (*Relationship, bool) { + if o == nil || o.Tuple == nil { + return nil, false + } + return o.Tuple, true +} + +// HasTuple returns a boolean if a field has been set. +func (o *ExpandedPermissionTree) HasTuple() bool { + if o != nil && o.Tuple != nil { + return true + } + + return false +} + +// SetTuple gets a reference to the given Relationship and assigns it to the Tuple field. +func (o *ExpandedPermissionTree) SetTuple(v Relationship) { + o.Tuple = &v +} + +// GetType returns the Type field value +func (o *ExpandedPermissionTree) GetType() string { + if o == nil { + var ret string + return ret + } + + return o.Type +} + +// GetTypeOk returns a tuple with the Type field value +// and a boolean to check if the value has been set. +func (o *ExpandedPermissionTree) GetTypeOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Type, true +} + +// SetType sets field value +func (o *ExpandedPermissionTree) SetType(v string) { + o.Type = v +} + +func (o ExpandedPermissionTree) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Children != nil { + toSerialize["children"] = o.Children + } + if o.Tuple != nil { + toSerialize["tuple"] = o.Tuple + } + if true { + toSerialize["type"] = o.Type + } + return json.Marshal(toSerialize) +} + +type NullableExpandedPermissionTree struct { + value *ExpandedPermissionTree + isSet bool +} + +func (v NullableExpandedPermissionTree) Get() *ExpandedPermissionTree { + return v.value +} + +func (v *NullableExpandedPermissionTree) Set(val *ExpandedPermissionTree) { + v.value = val + v.isSet = true +} + +func (v NullableExpandedPermissionTree) IsSet() bool { + return v.isSet +} + +func (v *NullableExpandedPermissionTree) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableExpandedPermissionTree(val *ExpandedPermissionTree) *NullableExpandedPermissionTree { + return &NullableExpandedPermissionTree{value: val, isSet: true} +} + +func (v NullableExpandedPermissionTree) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableExpandedPermissionTree) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_generic_error.go b/internal/httpclient/model_generic_error.go new file mode 100644 index 0000000..2e6bd18 --- /dev/null +++ b/internal/httpclient/model_generic_error.go @@ -0,0 +1,367 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// GenericError struct for GenericError +type GenericError struct { + // The status code + Code *int64 `json:"code,omitempty"` + // Debug information This field is often not exposed to protect against leaking sensitive information. + Debug *string `json:"debug,omitempty"` + // Further error details + Details map[string]interface{} `json:"details,omitempty"` + // The error ID Useful when trying to identify various errors in application logic. + Id *string `json:"id,omitempty"` + // Error message The error's message. + Message string `json:"message"` + // A human-readable reason for the error + Reason *string `json:"reason,omitempty"` + // The request ID The request ID is often exposed internally in order to trace errors across service architectures. This is often a UUID. + Request *string `json:"request,omitempty"` + // The status description + Status *string `json:"status,omitempty"` +} + +// NewGenericError instantiates a new GenericError object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewGenericError(message string) *GenericError { + this := GenericError{} + this.Message = message + return &this +} + +// NewGenericErrorWithDefaults instantiates a new GenericError object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewGenericErrorWithDefaults() *GenericError { + this := GenericError{} + return &this +} + +// GetCode returns the Code field value if set, zero value otherwise. +func (o *GenericError) GetCode() int64 { + if o == nil || o.Code == nil { + var ret int64 + return ret + } + return *o.Code +} + +// GetCodeOk returns a tuple with the Code field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetCodeOk() (*int64, bool) { + if o == nil || o.Code == nil { + return nil, false + } + return o.Code, true +} + +// HasCode returns a boolean if a field has been set. +func (o *GenericError) HasCode() bool { + if o != nil && o.Code != nil { + return true + } + + return false +} + +// SetCode gets a reference to the given int64 and assigns it to the Code field. +func (o *GenericError) SetCode(v int64) { + o.Code = &v +} + +// GetDebug returns the Debug field value if set, zero value otherwise. +func (o *GenericError) GetDebug() string { + if o == nil || o.Debug == nil { + var ret string + return ret + } + return *o.Debug +} + +// GetDebugOk returns a tuple with the Debug field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDebugOk() (*string, bool) { + if o == nil || o.Debug == nil { + return nil, false + } + return o.Debug, true +} + +// HasDebug returns a boolean if a field has been set. +func (o *GenericError) HasDebug() bool { + if o != nil && o.Debug != nil { + return true + } + + return false +} + +// SetDebug gets a reference to the given string and assigns it to the Debug field. +func (o *GenericError) SetDebug(v string) { + o.Debug = &v +} + +// GetDetails returns the Details field value if set, zero value otherwise. +func (o *GenericError) GetDetails() map[string]interface{} { + if o == nil || o.Details == nil { + var ret map[string]interface{} + return ret + } + return o.Details +} + +// GetDetailsOk returns a tuple with the Details field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetDetailsOk() (map[string]interface{}, bool) { + if o == nil || o.Details == nil { + return nil, false + } + return o.Details, true +} + +// HasDetails returns a boolean if a field has been set. +func (o *GenericError) HasDetails() bool { + if o != nil && o.Details != nil { + return true + } + + return false +} + +// SetDetails gets a reference to the given map[string]interface{} and assigns it to the Details field. +func (o *GenericError) SetDetails(v map[string]interface{}) { + o.Details = v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *GenericError) GetId() string { + if o == nil || o.Id == nil { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetIdOk() (*string, bool) { + if o == nil || o.Id == nil { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *GenericError) HasId() bool { + if o != nil && o.Id != nil { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *GenericError) SetId(v string) { + o.Id = &v +} + +// GetMessage returns the Message field value +func (o *GenericError) GetMessage() string { + if o == nil { + var ret string + return ret + } + + return o.Message +} + +// GetMessageOk returns a tuple with the Message field value +// and a boolean to check if the value has been set. +func (o *GenericError) GetMessageOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Message, true +} + +// SetMessage sets field value +func (o *GenericError) SetMessage(v string) { + o.Message = v +} + +// GetReason returns the Reason field value if set, zero value otherwise. +func (o *GenericError) GetReason() string { + if o == nil || o.Reason == nil { + var ret string + return ret + } + return *o.Reason +} + +// GetReasonOk returns a tuple with the Reason field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetReasonOk() (*string, bool) { + if o == nil || o.Reason == nil { + return nil, false + } + return o.Reason, true +} + +// HasReason returns a boolean if a field has been set. +func (o *GenericError) HasReason() bool { + if o != nil && o.Reason != nil { + return true + } + + return false +} + +// SetReason gets a reference to the given string and assigns it to the Reason field. +func (o *GenericError) SetReason(v string) { + o.Reason = &v +} + +// GetRequest returns the Request field value if set, zero value otherwise. +func (o *GenericError) GetRequest() string { + if o == nil || o.Request == nil { + var ret string + return ret + } + return *o.Request +} + +// GetRequestOk returns a tuple with the Request field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetRequestOk() (*string, bool) { + if o == nil || o.Request == nil { + return nil, false + } + return o.Request, true +} + +// HasRequest returns a boolean if a field has been set. +func (o *GenericError) HasRequest() bool { + if o != nil && o.Request != nil { + return true + } + + return false +} + +// SetRequest gets a reference to the given string and assigns it to the Request field. +func (o *GenericError) SetRequest(v string) { + o.Request = &v +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *GenericError) GetStatus() string { + if o == nil || o.Status == nil { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GenericError) GetStatusOk() (*string, bool) { + if o == nil || o.Status == nil { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *GenericError) HasStatus() bool { + if o != nil && o.Status != nil { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *GenericError) SetStatus(v string) { + o.Status = &v +} + +func (o GenericError) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Code != nil { + toSerialize["code"] = o.Code + } + if o.Debug != nil { + toSerialize["debug"] = o.Debug + } + if o.Details != nil { + toSerialize["details"] = o.Details + } + if o.Id != nil { + toSerialize["id"] = o.Id + } + if true { + toSerialize["message"] = o.Message + } + if o.Reason != nil { + toSerialize["reason"] = o.Reason + } + if o.Request != nil { + toSerialize["request"] = o.Request + } + if o.Status != nil { + toSerialize["status"] = o.Status + } + return json.Marshal(toSerialize) +} + +type NullableGenericError struct { + value *GenericError + isSet bool +} + +func (v NullableGenericError) Get() *GenericError { + return v.value +} + +func (v *NullableGenericError) Set(val *GenericError) { + v.value = val + v.isSet = true +} + +func (v NullableGenericError) IsSet() bool { + return v.isSet +} + +func (v *NullableGenericError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGenericError(val *GenericError) *NullableGenericError { + return &NullableGenericError{value: val, isSet: true} +} + +func (v NullableGenericError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGenericError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_get_version_200_response.go b/internal/httpclient/model_get_version_200_response.go new file mode 100644 index 0000000..3983b5b --- /dev/null +++ b/internal/httpclient/model_get_version_200_response.go @@ -0,0 +1,108 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// GetVersion200Response struct for GetVersion200Response +type GetVersion200Response struct { + // The version of Ory Keto. + Version string `json:"version"` +} + +// NewGetVersion200Response instantiates a new GetVersion200Response object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewGetVersion200Response(version string) *GetVersion200Response { + this := GetVersion200Response{} + this.Version = version + return &this +} + +// NewGetVersion200ResponseWithDefaults instantiates a new GetVersion200Response object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewGetVersion200ResponseWithDefaults() *GetVersion200Response { + this := GetVersion200Response{} + return &this +} + +// GetVersion returns the Version field value +func (o *GetVersion200Response) GetVersion() string { + if o == nil { + var ret string + return ret + } + + return o.Version +} + +// GetVersionOk returns a tuple with the Version field value +// and a boolean to check if the value has been set. +func (o *GetVersion200Response) GetVersionOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Version, true +} + +// SetVersion sets field value +func (o *GetVersion200Response) SetVersion(v string) { + o.Version = v +} + +func (o GetVersion200Response) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["version"] = o.Version + } + return json.Marshal(toSerialize) +} + +type NullableGetVersion200Response struct { + value *GetVersion200Response + isSet bool +} + +func (v NullableGetVersion200Response) Get() *GetVersion200Response { + return v.value +} + +func (v *NullableGetVersion200Response) Set(val *GetVersion200Response) { + v.value = val + v.isSet = true +} + +func (v NullableGetVersion200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableGetVersion200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGetVersion200Response(val *GetVersion200Response) *NullableGetVersion200Response { + return &NullableGetVersion200Response{value: val, isSet: true} +} + +func (v NullableGetVersion200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGetVersion200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_health_not_ready_status.go b/internal/httpclient/model_health_not_ready_status.go new file mode 100644 index 0000000..099dd85 --- /dev/null +++ b/internal/httpclient/model_health_not_ready_status.go @@ -0,0 +1,115 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// HealthNotReadyStatus struct for HealthNotReadyStatus +type HealthNotReadyStatus struct { + // Errors contains a list of errors that caused the not ready status. + Errors *map[string]string `json:"errors,omitempty"` +} + +// NewHealthNotReadyStatus instantiates a new HealthNotReadyStatus object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewHealthNotReadyStatus() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// NewHealthNotReadyStatusWithDefaults instantiates a new HealthNotReadyStatus object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewHealthNotReadyStatusWithDefaults() *HealthNotReadyStatus { + this := HealthNotReadyStatus{} + return &this +} + +// GetErrors returns the Errors field value if set, zero value otherwise. +func (o *HealthNotReadyStatus) GetErrors() map[string]string { + if o == nil || o.Errors == nil { + var ret map[string]string + return ret + } + return *o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthNotReadyStatus) GetErrorsOk() (*map[string]string, bool) { + if o == nil || o.Errors == nil { + return nil, false + } + return o.Errors, true +} + +// HasErrors returns a boolean if a field has been set. +func (o *HealthNotReadyStatus) HasErrors() bool { + if o != nil && o.Errors != nil { + return true + } + + return false +} + +// SetErrors gets a reference to the given map[string]string and assigns it to the Errors field. +func (o *HealthNotReadyStatus) SetErrors(v map[string]string) { + o.Errors = &v +} + +func (o HealthNotReadyStatus) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Errors != nil { + toSerialize["errors"] = o.Errors + } + return json.Marshal(toSerialize) +} + +type NullableHealthNotReadyStatus struct { + value *HealthNotReadyStatus + isSet bool +} + +func (v NullableHealthNotReadyStatus) Get() *HealthNotReadyStatus { + return v.value +} + +func (v *NullableHealthNotReadyStatus) Set(val *HealthNotReadyStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthNotReadyStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthNotReadyStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthNotReadyStatus(val *HealthNotReadyStatus) *NullableHealthNotReadyStatus { + return &NullableHealthNotReadyStatus{value: val, isSet: true} +} + +func (v NullableHealthNotReadyStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthNotReadyStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_health_status.go b/internal/httpclient/model_health_status.go new file mode 100644 index 0000000..288edb2 --- /dev/null +++ b/internal/httpclient/model_health_status.go @@ -0,0 +1,115 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// HealthStatus struct for HealthStatus +type HealthStatus struct { + // Status always contains \"ok\". + Status *string `json:"status,omitempty"` +} + +// NewHealthStatus instantiates a new HealthStatus object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewHealthStatus() *HealthStatus { + this := HealthStatus{} + return &this +} + +// NewHealthStatusWithDefaults instantiates a new HealthStatus object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewHealthStatusWithDefaults() *HealthStatus { + this := HealthStatus{} + return &this +} + +// GetStatus returns the Status field value if set, zero value otherwise. +func (o *HealthStatus) GetStatus() string { + if o == nil || o.Status == nil { + var ret string + return ret + } + return *o.Status +} + +// GetStatusOk returns a tuple with the Status field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *HealthStatus) GetStatusOk() (*string, bool) { + if o == nil || o.Status == nil { + return nil, false + } + return o.Status, true +} + +// HasStatus returns a boolean if a field has been set. +func (o *HealthStatus) HasStatus() bool { + if o != nil && o.Status != nil { + return true + } + + return false +} + +// SetStatus gets a reference to the given string and assigns it to the Status field. +func (o *HealthStatus) SetStatus(v string) { + o.Status = &v +} + +func (o HealthStatus) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Status != nil { + toSerialize["status"] = o.Status + } + return json.Marshal(toSerialize) +} + +type NullableHealthStatus struct { + value *HealthStatus + isSet bool +} + +func (v NullableHealthStatus) Get() *HealthStatus { + return v.value +} + +func (v *NullableHealthStatus) Set(val *HealthStatus) { + v.value = val + v.isSet = true +} + +func (v NullableHealthStatus) IsSet() bool { + return v.isSet +} + +func (v *NullableHealthStatus) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableHealthStatus(val *HealthStatus) *NullableHealthStatus { + return &NullableHealthStatus{value: val, isSet: true} +} + +func (v NullableHealthStatus) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableHealthStatus) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_is_alive_200_response.go b/internal/httpclient/model_is_alive_200_response.go new file mode 100644 index 0000000..772c4a0 --- /dev/null +++ b/internal/httpclient/model_is_alive_200_response.go @@ -0,0 +1,108 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IsAlive200Response struct for IsAlive200Response +type IsAlive200Response struct { + // Always \"ok\". + Status string `json:"status"` +} + +// NewIsAlive200Response instantiates a new IsAlive200Response object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIsAlive200Response(status string) *IsAlive200Response { + this := IsAlive200Response{} + this.Status = status + return &this +} + +// NewIsAlive200ResponseWithDefaults instantiates a new IsAlive200Response object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIsAlive200ResponseWithDefaults() *IsAlive200Response { + this := IsAlive200Response{} + return &this +} + +// GetStatus returns the Status field value +func (o *IsAlive200Response) GetStatus() string { + if o == nil { + var ret string + return ret + } + + return o.Status +} + +// GetStatusOk returns a tuple with the Status field value +// and a boolean to check if the value has been set. +func (o *IsAlive200Response) GetStatusOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Status, true +} + +// SetStatus sets field value +func (o *IsAlive200Response) SetStatus(v string) { + o.Status = v +} + +func (o IsAlive200Response) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["status"] = o.Status + } + return json.Marshal(toSerialize) +} + +type NullableIsAlive200Response struct { + value *IsAlive200Response + isSet bool +} + +func (v NullableIsAlive200Response) Get() *IsAlive200Response { + return v.value +} + +func (v *NullableIsAlive200Response) Set(val *IsAlive200Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsAlive200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsAlive200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsAlive200Response(val *IsAlive200Response) *NullableIsAlive200Response { + return &NullableIsAlive200Response{value: val, isSet: true} +} + +func (v NullableIsAlive200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsAlive200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_is_ready_503_response.go b/internal/httpclient/model_is_ready_503_response.go new file mode 100644 index 0000000..2890f2b --- /dev/null +++ b/internal/httpclient/model_is_ready_503_response.go @@ -0,0 +1,108 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// IsReady503Response struct for IsReady503Response +type IsReady503Response struct { + // Errors contains a list of errors that caused the not ready status. + Errors map[string]string `json:"errors"` +} + +// NewIsReady503Response instantiates a new IsReady503Response object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewIsReady503Response(errors map[string]string) *IsReady503Response { + this := IsReady503Response{} + this.Errors = errors + return &this +} + +// NewIsReady503ResponseWithDefaults instantiates a new IsReady503Response object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewIsReady503ResponseWithDefaults() *IsReady503Response { + this := IsReady503Response{} + return &this +} + +// GetErrors returns the Errors field value +func (o *IsReady503Response) GetErrors() map[string]string { + if o == nil { + var ret map[string]string + return ret + } + + return o.Errors +} + +// GetErrorsOk returns a tuple with the Errors field value +// and a boolean to check if the value has been set. +func (o *IsReady503Response) GetErrorsOk() (*map[string]string, bool) { + if o == nil { + return nil, false + } + return &o.Errors, true +} + +// SetErrors sets field value +func (o *IsReady503Response) SetErrors(v map[string]string) { + o.Errors = v +} + +func (o IsReady503Response) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["errors"] = o.Errors + } + return json.Marshal(toSerialize) +} + +type NullableIsReady503Response struct { + value *IsReady503Response + isSet bool +} + +func (v NullableIsReady503Response) Get() *IsReady503Response { + return v.value +} + +func (v *NullableIsReady503Response) Set(val *IsReady503Response) { + v.value = val + v.isSet = true +} + +func (v NullableIsReady503Response) IsSet() bool { + return v.isSet +} + +func (v *NullableIsReady503Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableIsReady503Response(val *IsReady503Response) *NullableIsReady503Response { + return &NullableIsReady503Response{value: val, isSet: true} +} + +func (v NullableIsReady503Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableIsReady503Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_namespace.go b/internal/httpclient/model_namespace.go new file mode 100644 index 0000000..6757cfd --- /dev/null +++ b/internal/httpclient/model_namespace.go @@ -0,0 +1,115 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// Namespace struct for Namespace +type Namespace struct { + // Name of the namespace. + Name *string `json:"name,omitempty"` +} + +// NewNamespace instantiates a new Namespace object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewNamespace() *Namespace { + this := Namespace{} + return &this +} + +// NewNamespaceWithDefaults instantiates a new Namespace object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewNamespaceWithDefaults() *Namespace { + this := Namespace{} + return &this +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *Namespace) GetName() string { + if o == nil || o.Name == nil { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Namespace) GetNameOk() (*string, bool) { + if o == nil || o.Name == nil { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *Namespace) HasName() bool { + if o != nil && o.Name != nil { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *Namespace) SetName(v string) { + o.Name = &v +} + +func (o Namespace) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Name != nil { + toSerialize["name"] = o.Name + } + return json.Marshal(toSerialize) +} + +type NullableNamespace struct { + value *Namespace + isSet bool +} + +func (v NullableNamespace) Get() *Namespace { + return v.value +} + +func (v *NullableNamespace) Set(val *Namespace) { + v.value = val + v.isSet = true +} + +func (v NullableNamespace) IsSet() bool { + return v.isSet +} + +func (v *NullableNamespace) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableNamespace(val *Namespace) *NullableNamespace { + return &NullableNamespace{value: val, isSet: true} +} + +func (v NullableNamespace) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableNamespace) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_parse_error.go b/internal/httpclient/model_parse_error.go new file mode 100644 index 0000000..35bdb4d --- /dev/null +++ b/internal/httpclient/model_parse_error.go @@ -0,0 +1,186 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// ParseError struct for ParseError +type ParseError struct { + End *SourcePosition `json:"end,omitempty"` + Message *string `json:"message,omitempty"` + Start *SourcePosition `json:"start,omitempty"` +} + +// NewParseError instantiates a new ParseError object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewParseError() *ParseError { + this := ParseError{} + return &this +} + +// NewParseErrorWithDefaults instantiates a new ParseError object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewParseErrorWithDefaults() *ParseError { + this := ParseError{} + return &this +} + +// GetEnd returns the End field value if set, zero value otherwise. +func (o *ParseError) GetEnd() SourcePosition { + if o == nil || o.End == nil { + var ret SourcePosition + return ret + } + return *o.End +} + +// GetEndOk returns a tuple with the End field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ParseError) GetEndOk() (*SourcePosition, bool) { + if o == nil || o.End == nil { + return nil, false + } + return o.End, true +} + +// HasEnd returns a boolean if a field has been set. +func (o *ParseError) HasEnd() bool { + if o != nil && o.End != nil { + return true + } + + return false +} + +// SetEnd gets a reference to the given SourcePosition and assigns it to the End field. +func (o *ParseError) SetEnd(v SourcePosition) { + o.End = &v +} + +// GetMessage returns the Message field value if set, zero value otherwise. +func (o *ParseError) GetMessage() string { + if o == nil || o.Message == nil { + var ret string + return ret + } + return *o.Message +} + +// GetMessageOk returns a tuple with the Message field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ParseError) GetMessageOk() (*string, bool) { + if o == nil || o.Message == nil { + return nil, false + } + return o.Message, true +} + +// HasMessage returns a boolean if a field has been set. +func (o *ParseError) HasMessage() bool { + if o != nil && o.Message != nil { + return true + } + + return false +} + +// SetMessage gets a reference to the given string and assigns it to the Message field. +func (o *ParseError) SetMessage(v string) { + o.Message = &v +} + +// GetStart returns the Start field value if set, zero value otherwise. +func (o *ParseError) GetStart() SourcePosition { + if o == nil || o.Start == nil { + var ret SourcePosition + return ret + } + return *o.Start +} + +// GetStartOk returns a tuple with the Start field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ParseError) GetStartOk() (*SourcePosition, bool) { + if o == nil || o.Start == nil { + return nil, false + } + return o.Start, true +} + +// HasStart returns a boolean if a field has been set. +func (o *ParseError) HasStart() bool { + if o != nil && o.Start != nil { + return true + } + + return false +} + +// SetStart gets a reference to the given SourcePosition and assigns it to the Start field. +func (o *ParseError) SetStart(v SourcePosition) { + o.Start = &v +} + +func (o ParseError) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.End != nil { + toSerialize["end"] = o.End + } + if o.Message != nil { + toSerialize["message"] = o.Message + } + if o.Start != nil { + toSerialize["start"] = o.Start + } + return json.Marshal(toSerialize) +} + +type NullableParseError struct { + value *ParseError + isSet bool +} + +func (v NullableParseError) Get() *ParseError { + return v.value +} + +func (v *NullableParseError) Set(val *ParseError) { + v.value = val + v.isSet = true +} + +func (v NullableParseError) IsSet() bool { + return v.isSet +} + +func (v *NullableParseError) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableParseError(val *ParseError) *NullableParseError { + return &NullableParseError{value: val, isSet: true} +} + +func (v NullableParseError) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableParseError) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_post_check_permission_body.go b/internal/httpclient/model_post_check_permission_body.go new file mode 100644 index 0000000..80286f7 --- /dev/null +++ b/internal/httpclient/model_post_check_permission_body.go @@ -0,0 +1,262 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// PostCheckPermissionBody Check Permission using Post Request Body +type PostCheckPermissionBody struct { + // Namespace to query + Namespace *string `json:"namespace,omitempty"` + // Object to query + Object *string `json:"object,omitempty"` + // Relation to query + Relation *string `json:"relation,omitempty"` + // SubjectID to query Either SubjectSet or SubjectID can be provided. + SubjectId *string `json:"subject_id,omitempty"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// NewPostCheckPermissionBody instantiates a new PostCheckPermissionBody object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewPostCheckPermissionBody() *PostCheckPermissionBody { + this := PostCheckPermissionBody{} + return &this +} + +// NewPostCheckPermissionBodyWithDefaults instantiates a new PostCheckPermissionBody object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewPostCheckPermissionBodyWithDefaults() *PostCheckPermissionBody { + this := PostCheckPermissionBody{} + return &this +} + +// GetNamespace returns the Namespace field value if set, zero value otherwise. +func (o *PostCheckPermissionBody) GetNamespace() string { + if o == nil || o.Namespace == nil { + var ret string + return ret + } + return *o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionBody) GetNamespaceOk() (*string, bool) { + if o == nil || o.Namespace == nil { + return nil, false + } + return o.Namespace, true +} + +// HasNamespace returns a boolean if a field has been set. +func (o *PostCheckPermissionBody) HasNamespace() bool { + if o != nil && o.Namespace != nil { + return true + } + + return false +} + +// SetNamespace gets a reference to the given string and assigns it to the Namespace field. +func (o *PostCheckPermissionBody) SetNamespace(v string) { + o.Namespace = &v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *PostCheckPermissionBody) GetObject() string { + if o == nil || o.Object == nil { + var ret string + return ret + } + return *o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionBody) GetObjectOk() (*string, bool) { + if o == nil || o.Object == nil { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *PostCheckPermissionBody) HasObject() bool { + if o != nil && o.Object != nil { + return true + } + + return false +} + +// SetObject gets a reference to the given string and assigns it to the Object field. +func (o *PostCheckPermissionBody) SetObject(v string) { + o.Object = &v +} + +// GetRelation returns the Relation field value if set, zero value otherwise. +func (o *PostCheckPermissionBody) GetRelation() string { + if o == nil || o.Relation == nil { + var ret string + return ret + } + return *o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionBody) GetRelationOk() (*string, bool) { + if o == nil || o.Relation == nil { + return nil, false + } + return o.Relation, true +} + +// HasRelation returns a boolean if a field has been set. +func (o *PostCheckPermissionBody) HasRelation() bool { + if o != nil && o.Relation != nil { + return true + } + + return false +} + +// SetRelation gets a reference to the given string and assigns it to the Relation field. +func (o *PostCheckPermissionBody) SetRelation(v string) { + o.Relation = &v +} + +// GetSubjectId returns the SubjectId field value if set, zero value otherwise. +func (o *PostCheckPermissionBody) GetSubjectId() string { + if o == nil || o.SubjectId == nil { + var ret string + return ret + } + return *o.SubjectId +} + +// GetSubjectIdOk returns a tuple with the SubjectId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionBody) GetSubjectIdOk() (*string, bool) { + if o == nil || o.SubjectId == nil { + return nil, false + } + return o.SubjectId, true +} + +// HasSubjectId returns a boolean if a field has been set. +func (o *PostCheckPermissionBody) HasSubjectId() bool { + if o != nil && o.SubjectId != nil { + return true + } + + return false +} + +// SetSubjectId gets a reference to the given string and assigns it to the SubjectId field. +func (o *PostCheckPermissionBody) SetSubjectId(v string) { + o.SubjectId = &v +} + +// GetSubjectSet returns the SubjectSet field value if set, zero value otherwise. +func (o *PostCheckPermissionBody) GetSubjectSet() SubjectSet { + if o == nil || o.SubjectSet == nil { + var ret SubjectSet + return ret + } + return *o.SubjectSet +} + +// GetSubjectSetOk returns a tuple with the SubjectSet field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionBody) GetSubjectSetOk() (*SubjectSet, bool) { + if o == nil || o.SubjectSet == nil { + return nil, false + } + return o.SubjectSet, true +} + +// HasSubjectSet returns a boolean if a field has been set. +func (o *PostCheckPermissionBody) HasSubjectSet() bool { + if o != nil && o.SubjectSet != nil { + return true + } + + return false +} + +// SetSubjectSet gets a reference to the given SubjectSet and assigns it to the SubjectSet field. +func (o *PostCheckPermissionBody) SetSubjectSet(v SubjectSet) { + o.SubjectSet = &v +} + +func (o PostCheckPermissionBody) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Namespace != nil { + toSerialize["namespace"] = o.Namespace + } + if o.Object != nil { + toSerialize["object"] = o.Object + } + if o.Relation != nil { + toSerialize["relation"] = o.Relation + } + if o.SubjectId != nil { + toSerialize["subject_id"] = o.SubjectId + } + if o.SubjectSet != nil { + toSerialize["subject_set"] = o.SubjectSet + } + return json.Marshal(toSerialize) +} + +type NullablePostCheckPermissionBody struct { + value *PostCheckPermissionBody + isSet bool +} + +func (v NullablePostCheckPermissionBody) Get() *PostCheckPermissionBody { + return v.value +} + +func (v *NullablePostCheckPermissionBody) Set(val *PostCheckPermissionBody) { + v.value = val + v.isSet = true +} + +func (v NullablePostCheckPermissionBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePostCheckPermissionBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePostCheckPermissionBody(val *PostCheckPermissionBody) *NullablePostCheckPermissionBody { + return &NullablePostCheckPermissionBody{value: val, isSet: true} +} + +func (v NullablePostCheckPermissionBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePostCheckPermissionBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_post_check_permission_or_error_body.go b/internal/httpclient/model_post_check_permission_or_error_body.go new file mode 100644 index 0000000..f359ae3 --- /dev/null +++ b/internal/httpclient/model_post_check_permission_or_error_body.go @@ -0,0 +1,262 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// PostCheckPermissionOrErrorBody Post Check Permission Or Error Body +type PostCheckPermissionOrErrorBody struct { + // Namespace to query + Namespace *string `json:"namespace,omitempty"` + // Object to query + Object *string `json:"object,omitempty"` + // Relation to query + Relation *string `json:"relation,omitempty"` + // SubjectID to query Either SubjectSet or SubjectID can be provided. + SubjectId *string `json:"subject_id,omitempty"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// NewPostCheckPermissionOrErrorBody instantiates a new PostCheckPermissionOrErrorBody object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewPostCheckPermissionOrErrorBody() *PostCheckPermissionOrErrorBody { + this := PostCheckPermissionOrErrorBody{} + return &this +} + +// NewPostCheckPermissionOrErrorBodyWithDefaults instantiates a new PostCheckPermissionOrErrorBody object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewPostCheckPermissionOrErrorBodyWithDefaults() *PostCheckPermissionOrErrorBody { + this := PostCheckPermissionOrErrorBody{} + return &this +} + +// GetNamespace returns the Namespace field value if set, zero value otherwise. +func (o *PostCheckPermissionOrErrorBody) GetNamespace() string { + if o == nil || o.Namespace == nil { + var ret string + return ret + } + return *o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionOrErrorBody) GetNamespaceOk() (*string, bool) { + if o == nil || o.Namespace == nil { + return nil, false + } + return o.Namespace, true +} + +// HasNamespace returns a boolean if a field has been set. +func (o *PostCheckPermissionOrErrorBody) HasNamespace() bool { + if o != nil && o.Namespace != nil { + return true + } + + return false +} + +// SetNamespace gets a reference to the given string and assigns it to the Namespace field. +func (o *PostCheckPermissionOrErrorBody) SetNamespace(v string) { + o.Namespace = &v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *PostCheckPermissionOrErrorBody) GetObject() string { + if o == nil || o.Object == nil { + var ret string + return ret + } + return *o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionOrErrorBody) GetObjectOk() (*string, bool) { + if o == nil || o.Object == nil { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *PostCheckPermissionOrErrorBody) HasObject() bool { + if o != nil && o.Object != nil { + return true + } + + return false +} + +// SetObject gets a reference to the given string and assigns it to the Object field. +func (o *PostCheckPermissionOrErrorBody) SetObject(v string) { + o.Object = &v +} + +// GetRelation returns the Relation field value if set, zero value otherwise. +func (o *PostCheckPermissionOrErrorBody) GetRelation() string { + if o == nil || o.Relation == nil { + var ret string + return ret + } + return *o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionOrErrorBody) GetRelationOk() (*string, bool) { + if o == nil || o.Relation == nil { + return nil, false + } + return o.Relation, true +} + +// HasRelation returns a boolean if a field has been set. +func (o *PostCheckPermissionOrErrorBody) HasRelation() bool { + if o != nil && o.Relation != nil { + return true + } + + return false +} + +// SetRelation gets a reference to the given string and assigns it to the Relation field. +func (o *PostCheckPermissionOrErrorBody) SetRelation(v string) { + o.Relation = &v +} + +// GetSubjectId returns the SubjectId field value if set, zero value otherwise. +func (o *PostCheckPermissionOrErrorBody) GetSubjectId() string { + if o == nil || o.SubjectId == nil { + var ret string + return ret + } + return *o.SubjectId +} + +// GetSubjectIdOk returns a tuple with the SubjectId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionOrErrorBody) GetSubjectIdOk() (*string, bool) { + if o == nil || o.SubjectId == nil { + return nil, false + } + return o.SubjectId, true +} + +// HasSubjectId returns a boolean if a field has been set. +func (o *PostCheckPermissionOrErrorBody) HasSubjectId() bool { + if o != nil && o.SubjectId != nil { + return true + } + + return false +} + +// SetSubjectId gets a reference to the given string and assigns it to the SubjectId field. +func (o *PostCheckPermissionOrErrorBody) SetSubjectId(v string) { + o.SubjectId = &v +} + +// GetSubjectSet returns the SubjectSet field value if set, zero value otherwise. +func (o *PostCheckPermissionOrErrorBody) GetSubjectSet() SubjectSet { + if o == nil || o.SubjectSet == nil { + var ret SubjectSet + return ret + } + return *o.SubjectSet +} + +// GetSubjectSetOk returns a tuple with the SubjectSet field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *PostCheckPermissionOrErrorBody) GetSubjectSetOk() (*SubjectSet, bool) { + if o == nil || o.SubjectSet == nil { + return nil, false + } + return o.SubjectSet, true +} + +// HasSubjectSet returns a boolean if a field has been set. +func (o *PostCheckPermissionOrErrorBody) HasSubjectSet() bool { + if o != nil && o.SubjectSet != nil { + return true + } + + return false +} + +// SetSubjectSet gets a reference to the given SubjectSet and assigns it to the SubjectSet field. +func (o *PostCheckPermissionOrErrorBody) SetSubjectSet(v SubjectSet) { + o.SubjectSet = &v +} + +func (o PostCheckPermissionOrErrorBody) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Namespace != nil { + toSerialize["namespace"] = o.Namespace + } + if o.Object != nil { + toSerialize["object"] = o.Object + } + if o.Relation != nil { + toSerialize["relation"] = o.Relation + } + if o.SubjectId != nil { + toSerialize["subject_id"] = o.SubjectId + } + if o.SubjectSet != nil { + toSerialize["subject_set"] = o.SubjectSet + } + return json.Marshal(toSerialize) +} + +type NullablePostCheckPermissionOrErrorBody struct { + value *PostCheckPermissionOrErrorBody + isSet bool +} + +func (v NullablePostCheckPermissionOrErrorBody) Get() *PostCheckPermissionOrErrorBody { + return v.value +} + +func (v *NullablePostCheckPermissionOrErrorBody) Set(val *PostCheckPermissionOrErrorBody) { + v.value = val + v.isSet = true +} + +func (v NullablePostCheckPermissionOrErrorBody) IsSet() bool { + return v.isSet +} + +func (v *NullablePostCheckPermissionOrErrorBody) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullablePostCheckPermissionOrErrorBody(val *PostCheckPermissionOrErrorBody) *NullablePostCheckPermissionOrErrorBody { + return &NullablePostCheckPermissionOrErrorBody{value: val, isSet: true} +} + +func (v NullablePostCheckPermissionOrErrorBody) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullablePostCheckPermissionOrErrorBody) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_relation_query.go b/internal/httpclient/model_relation_query.go new file mode 100644 index 0000000..b6c1ab1 --- /dev/null +++ b/internal/httpclient/model_relation_query.go @@ -0,0 +1,262 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// RelationQuery Relation Query +type RelationQuery struct { + // Namespace to query + Namespace *string `json:"namespace,omitempty"` + // Object to query + Object *string `json:"object,omitempty"` + // Relation to query + Relation *string `json:"relation,omitempty"` + // SubjectID to query Either SubjectSet or SubjectID can be provided. + SubjectId *string `json:"subject_id,omitempty"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// NewRelationQuery instantiates a new RelationQuery object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRelationQuery() *RelationQuery { + this := RelationQuery{} + return &this +} + +// NewRelationQueryWithDefaults instantiates a new RelationQuery object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRelationQueryWithDefaults() *RelationQuery { + this := RelationQuery{} + return &this +} + +// GetNamespace returns the Namespace field value if set, zero value otherwise. +func (o *RelationQuery) GetNamespace() string { + if o == nil || o.Namespace == nil { + var ret string + return ret + } + return *o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationQuery) GetNamespaceOk() (*string, bool) { + if o == nil || o.Namespace == nil { + return nil, false + } + return o.Namespace, true +} + +// HasNamespace returns a boolean if a field has been set. +func (o *RelationQuery) HasNamespace() bool { + if o != nil && o.Namespace != nil { + return true + } + + return false +} + +// SetNamespace gets a reference to the given string and assigns it to the Namespace field. +func (o *RelationQuery) SetNamespace(v string) { + o.Namespace = &v +} + +// GetObject returns the Object field value if set, zero value otherwise. +func (o *RelationQuery) GetObject() string { + if o == nil || o.Object == nil { + var ret string + return ret + } + return *o.Object +} + +// GetObjectOk returns a tuple with the Object field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationQuery) GetObjectOk() (*string, bool) { + if o == nil || o.Object == nil { + return nil, false + } + return o.Object, true +} + +// HasObject returns a boolean if a field has been set. +func (o *RelationQuery) HasObject() bool { + if o != nil && o.Object != nil { + return true + } + + return false +} + +// SetObject gets a reference to the given string and assigns it to the Object field. +func (o *RelationQuery) SetObject(v string) { + o.Object = &v +} + +// GetRelation returns the Relation field value if set, zero value otherwise. +func (o *RelationQuery) GetRelation() string { + if o == nil || o.Relation == nil { + var ret string + return ret + } + return *o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationQuery) GetRelationOk() (*string, bool) { + if o == nil || o.Relation == nil { + return nil, false + } + return o.Relation, true +} + +// HasRelation returns a boolean if a field has been set. +func (o *RelationQuery) HasRelation() bool { + if o != nil && o.Relation != nil { + return true + } + + return false +} + +// SetRelation gets a reference to the given string and assigns it to the Relation field. +func (o *RelationQuery) SetRelation(v string) { + o.Relation = &v +} + +// GetSubjectId returns the SubjectId field value if set, zero value otherwise. +func (o *RelationQuery) GetSubjectId() string { + if o == nil || o.SubjectId == nil { + var ret string + return ret + } + return *o.SubjectId +} + +// GetSubjectIdOk returns a tuple with the SubjectId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationQuery) GetSubjectIdOk() (*string, bool) { + if o == nil || o.SubjectId == nil { + return nil, false + } + return o.SubjectId, true +} + +// HasSubjectId returns a boolean if a field has been set. +func (o *RelationQuery) HasSubjectId() bool { + if o != nil && o.SubjectId != nil { + return true + } + + return false +} + +// SetSubjectId gets a reference to the given string and assigns it to the SubjectId field. +func (o *RelationQuery) SetSubjectId(v string) { + o.SubjectId = &v +} + +// GetSubjectSet returns the SubjectSet field value if set, zero value otherwise. +func (o *RelationQuery) GetSubjectSet() SubjectSet { + if o == nil || o.SubjectSet == nil { + var ret SubjectSet + return ret + } + return *o.SubjectSet +} + +// GetSubjectSetOk returns a tuple with the SubjectSet field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationQuery) GetSubjectSetOk() (*SubjectSet, bool) { + if o == nil || o.SubjectSet == nil { + return nil, false + } + return o.SubjectSet, true +} + +// HasSubjectSet returns a boolean if a field has been set. +func (o *RelationQuery) HasSubjectSet() bool { + if o != nil && o.SubjectSet != nil { + return true + } + + return false +} + +// SetSubjectSet gets a reference to the given SubjectSet and assigns it to the SubjectSet field. +func (o *RelationQuery) SetSubjectSet(v SubjectSet) { + o.SubjectSet = &v +} + +func (o RelationQuery) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Namespace != nil { + toSerialize["namespace"] = o.Namespace + } + if o.Object != nil { + toSerialize["object"] = o.Object + } + if o.Relation != nil { + toSerialize["relation"] = o.Relation + } + if o.SubjectId != nil { + toSerialize["subject_id"] = o.SubjectId + } + if o.SubjectSet != nil { + toSerialize["subject_set"] = o.SubjectSet + } + return json.Marshal(toSerialize) +} + +type NullableRelationQuery struct { + value *RelationQuery + isSet bool +} + +func (v NullableRelationQuery) Get() *RelationQuery { + return v.value +} + +func (v *NullableRelationQuery) Set(val *RelationQuery) { + v.value = val + v.isSet = true +} + +func (v NullableRelationQuery) IsSet() bool { + return v.isSet +} + +func (v *NullableRelationQuery) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRelationQuery(val *RelationQuery) *NullableRelationQuery { + return &NullableRelationQuery{value: val, isSet: true} +} + +func (v NullableRelationQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRelationQuery) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_relationship.go b/internal/httpclient/model_relationship.go new file mode 100644 index 0000000..3717c98 --- /dev/null +++ b/internal/httpclient/model_relationship.go @@ -0,0 +1,241 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// Relationship Relationship +type Relationship struct { + // Namespace of the Relation Tuple + Namespace string `json:"namespace"` + // Object of the Relation Tuple + Object string `json:"object"` + // Relation of the Relation Tuple + Relation string `json:"relation"` + // SubjectID of the Relation Tuple Either SubjectSet or SubjectID can be provided. + SubjectId *string `json:"subject_id,omitempty"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// NewRelationship instantiates a new Relationship object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRelationship(namespace string, object string, relation string) *Relationship { + this := Relationship{} + this.Namespace = namespace + this.Object = object + this.Relation = relation + return &this +} + +// NewRelationshipWithDefaults instantiates a new Relationship object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRelationshipWithDefaults() *Relationship { + this := Relationship{} + return &this +} + +// GetNamespace returns the Namespace field value +func (o *Relationship) GetNamespace() string { + if o == nil { + var ret string + return ret + } + + return o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value +// and a boolean to check if the value has been set. +func (o *Relationship) GetNamespaceOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Namespace, true +} + +// SetNamespace sets field value +func (o *Relationship) SetNamespace(v string) { + o.Namespace = v +} + +// GetObject returns the Object field value +func (o *Relationship) GetObject() string { + if o == nil { + var ret string + return ret + } + + return o.Object +} + +// GetObjectOk returns a tuple with the Object field value +// and a boolean to check if the value has been set. +func (o *Relationship) GetObjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Object, true +} + +// SetObject sets field value +func (o *Relationship) SetObject(v string) { + o.Object = v +} + +// GetRelation returns the Relation field value +func (o *Relationship) GetRelation() string { + if o == nil { + var ret string + return ret + } + + return o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value +// and a boolean to check if the value has been set. +func (o *Relationship) GetRelationOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Relation, true +} + +// SetRelation sets field value +func (o *Relationship) SetRelation(v string) { + o.Relation = v +} + +// GetSubjectId returns the SubjectId field value if set, zero value otherwise. +func (o *Relationship) GetSubjectId() string { + if o == nil || o.SubjectId == nil { + var ret string + return ret + } + return *o.SubjectId +} + +// GetSubjectIdOk returns a tuple with the SubjectId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Relationship) GetSubjectIdOk() (*string, bool) { + if o == nil || o.SubjectId == nil { + return nil, false + } + return o.SubjectId, true +} + +// HasSubjectId returns a boolean if a field has been set. +func (o *Relationship) HasSubjectId() bool { + if o != nil && o.SubjectId != nil { + return true + } + + return false +} + +// SetSubjectId gets a reference to the given string and assigns it to the SubjectId field. +func (o *Relationship) SetSubjectId(v string) { + o.SubjectId = &v +} + +// GetSubjectSet returns the SubjectSet field value if set, zero value otherwise. +func (o *Relationship) GetSubjectSet() SubjectSet { + if o == nil || o.SubjectSet == nil { + var ret SubjectSet + return ret + } + return *o.SubjectSet +} + +// GetSubjectSetOk returns a tuple with the SubjectSet field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Relationship) GetSubjectSetOk() (*SubjectSet, bool) { + if o == nil || o.SubjectSet == nil { + return nil, false + } + return o.SubjectSet, true +} + +// HasSubjectSet returns a boolean if a field has been set. +func (o *Relationship) HasSubjectSet() bool { + if o != nil && o.SubjectSet != nil { + return true + } + + return false +} + +// SetSubjectSet gets a reference to the given SubjectSet and assigns it to the SubjectSet field. +func (o *Relationship) SetSubjectSet(v SubjectSet) { + o.SubjectSet = &v +} + +func (o Relationship) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["namespace"] = o.Namespace + } + if true { + toSerialize["object"] = o.Object + } + if true { + toSerialize["relation"] = o.Relation + } + if o.SubjectId != nil { + toSerialize["subject_id"] = o.SubjectId + } + if o.SubjectSet != nil { + toSerialize["subject_set"] = o.SubjectSet + } + return json.Marshal(toSerialize) +} + +type NullableRelationship struct { + value *Relationship + isSet bool +} + +func (v NullableRelationship) Get() *Relationship { + return v.value +} + +func (v *NullableRelationship) Set(val *Relationship) { + v.value = val + v.isSet = true +} + +func (v NullableRelationship) IsSet() bool { + return v.isSet +} + +func (v *NullableRelationship) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRelationship(val *Relationship) *NullableRelationship { + return &NullableRelationship{value: val, isSet: true} +} + +func (v NullableRelationship) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRelationship) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_relationship_namespaces.go b/internal/httpclient/model_relationship_namespaces.go new file mode 100644 index 0000000..5085ffe --- /dev/null +++ b/internal/httpclient/model_relationship_namespaces.go @@ -0,0 +1,114 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// RelationshipNamespaces Relationship Namespace List +type RelationshipNamespaces struct { + Namespaces []Namespace `json:"namespaces,omitempty"` +} + +// NewRelationshipNamespaces instantiates a new RelationshipNamespaces object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRelationshipNamespaces() *RelationshipNamespaces { + this := RelationshipNamespaces{} + return &this +} + +// NewRelationshipNamespacesWithDefaults instantiates a new RelationshipNamespaces object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRelationshipNamespacesWithDefaults() *RelationshipNamespaces { + this := RelationshipNamespaces{} + return &this +} + +// GetNamespaces returns the Namespaces field value if set, zero value otherwise. +func (o *RelationshipNamespaces) GetNamespaces() []Namespace { + if o == nil || o.Namespaces == nil { + var ret []Namespace + return ret + } + return o.Namespaces +} + +// GetNamespacesOk returns a tuple with the Namespaces field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationshipNamespaces) GetNamespacesOk() ([]Namespace, bool) { + if o == nil || o.Namespaces == nil { + return nil, false + } + return o.Namespaces, true +} + +// HasNamespaces returns a boolean if a field has been set. +func (o *RelationshipNamespaces) HasNamespaces() bool { + if o != nil && o.Namespaces != nil { + return true + } + + return false +} + +// SetNamespaces gets a reference to the given []Namespace and assigns it to the Namespaces field. +func (o *RelationshipNamespaces) SetNamespaces(v []Namespace) { + o.Namespaces = v +} + +func (o RelationshipNamespaces) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Namespaces != nil { + toSerialize["namespaces"] = o.Namespaces + } + return json.Marshal(toSerialize) +} + +type NullableRelationshipNamespaces struct { + value *RelationshipNamespaces + isSet bool +} + +func (v NullableRelationshipNamespaces) Get() *RelationshipNamespaces { + return v.value +} + +func (v *NullableRelationshipNamespaces) Set(val *RelationshipNamespaces) { + v.value = val + v.isSet = true +} + +func (v NullableRelationshipNamespaces) IsSet() bool { + return v.isSet +} + +func (v *NullableRelationshipNamespaces) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRelationshipNamespaces(val *RelationshipNamespaces) *NullableRelationshipNamespaces { + return &NullableRelationshipNamespaces{value: val, isSet: true} +} + +func (v NullableRelationshipNamespaces) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRelationshipNamespaces) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_relationship_patch.go b/internal/httpclient/model_relationship_patch.go new file mode 100644 index 0000000..77dea76 --- /dev/null +++ b/internal/httpclient/model_relationship_patch.go @@ -0,0 +1,150 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// RelationshipPatch Payload for patching a relationship +type RelationshipPatch struct { + Action *string `json:"action,omitempty"` + RelationTuple *Relationship `json:"relation_tuple,omitempty"` +} + +// NewRelationshipPatch instantiates a new RelationshipPatch object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRelationshipPatch() *RelationshipPatch { + this := RelationshipPatch{} + return &this +} + +// NewRelationshipPatchWithDefaults instantiates a new RelationshipPatch object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRelationshipPatchWithDefaults() *RelationshipPatch { + this := RelationshipPatch{} + return &this +} + +// GetAction returns the Action field value if set, zero value otherwise. +func (o *RelationshipPatch) GetAction() string { + if o == nil || o.Action == nil { + var ret string + return ret + } + return *o.Action +} + +// GetActionOk returns a tuple with the Action field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationshipPatch) GetActionOk() (*string, bool) { + if o == nil || o.Action == nil { + return nil, false + } + return o.Action, true +} + +// HasAction returns a boolean if a field has been set. +func (o *RelationshipPatch) HasAction() bool { + if o != nil && o.Action != nil { + return true + } + + return false +} + +// SetAction gets a reference to the given string and assigns it to the Action field. +func (o *RelationshipPatch) SetAction(v string) { + o.Action = &v +} + +// GetRelationTuple returns the RelationTuple field value if set, zero value otherwise. +func (o *RelationshipPatch) GetRelationTuple() Relationship { + if o == nil || o.RelationTuple == nil { + var ret Relationship + return ret + } + return *o.RelationTuple +} + +// GetRelationTupleOk returns a tuple with the RelationTuple field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *RelationshipPatch) GetRelationTupleOk() (*Relationship, bool) { + if o == nil || o.RelationTuple == nil { + return nil, false + } + return o.RelationTuple, true +} + +// HasRelationTuple returns a boolean if a field has been set. +func (o *RelationshipPatch) HasRelationTuple() bool { + if o != nil && o.RelationTuple != nil { + return true + } + + return false +} + +// SetRelationTuple gets a reference to the given Relationship and assigns it to the RelationTuple field. +func (o *RelationshipPatch) SetRelationTuple(v Relationship) { + o.RelationTuple = &v +} + +func (o RelationshipPatch) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Action != nil { + toSerialize["action"] = o.Action + } + if o.RelationTuple != nil { + toSerialize["relation_tuple"] = o.RelationTuple + } + return json.Marshal(toSerialize) +} + +type NullableRelationshipPatch struct { + value *RelationshipPatch + isSet bool +} + +func (v NullableRelationshipPatch) Get() *RelationshipPatch { + return v.value +} + +func (v *NullableRelationshipPatch) Set(val *RelationshipPatch) { + v.value = val + v.isSet = true +} + +func (v NullableRelationshipPatch) IsSet() bool { + return v.isSet +} + +func (v *NullableRelationshipPatch) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRelationshipPatch(val *RelationshipPatch) *NullableRelationshipPatch { + return &NullableRelationshipPatch{value: val, isSet: true} +} + +func (v NullableRelationshipPatch) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRelationshipPatch) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_relationships.go b/internal/httpclient/model_relationships.go new file mode 100644 index 0000000..d5f2c8e --- /dev/null +++ b/internal/httpclient/model_relationships.go @@ -0,0 +1,151 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// Relationships Paginated Relationship List +type Relationships struct { + // The opaque token to provide in a subsequent request to get the next page. It is the empty string iff this is the last page. + NextPageToken *string `json:"next_page_token,omitempty"` + RelationTuples []Relationship `json:"relation_tuples,omitempty"` +} + +// NewRelationships instantiates a new Relationships object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewRelationships() *Relationships { + this := Relationships{} + return &this +} + +// NewRelationshipsWithDefaults instantiates a new Relationships object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewRelationshipsWithDefaults() *Relationships { + this := Relationships{} + return &this +} + +// GetNextPageToken returns the NextPageToken field value if set, zero value otherwise. +func (o *Relationships) GetNextPageToken() string { + if o == nil || o.NextPageToken == nil { + var ret string + return ret + } + return *o.NextPageToken +} + +// GetNextPageTokenOk returns a tuple with the NextPageToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Relationships) GetNextPageTokenOk() (*string, bool) { + if o == nil || o.NextPageToken == nil { + return nil, false + } + return o.NextPageToken, true +} + +// HasNextPageToken returns a boolean if a field has been set. +func (o *Relationships) HasNextPageToken() bool { + if o != nil && o.NextPageToken != nil { + return true + } + + return false +} + +// SetNextPageToken gets a reference to the given string and assigns it to the NextPageToken field. +func (o *Relationships) SetNextPageToken(v string) { + o.NextPageToken = &v +} + +// GetRelationTuples returns the RelationTuples field value if set, zero value otherwise. +func (o *Relationships) GetRelationTuples() []Relationship { + if o == nil || o.RelationTuples == nil { + var ret []Relationship + return ret + } + return o.RelationTuples +} + +// GetRelationTuplesOk returns a tuple with the RelationTuples field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Relationships) GetRelationTuplesOk() ([]Relationship, bool) { + if o == nil || o.RelationTuples == nil { + return nil, false + } + return o.RelationTuples, true +} + +// HasRelationTuples returns a boolean if a field has been set. +func (o *Relationships) HasRelationTuples() bool { + if o != nil && o.RelationTuples != nil { + return true + } + + return false +} + +// SetRelationTuples gets a reference to the given []Relationship and assigns it to the RelationTuples field. +func (o *Relationships) SetRelationTuples(v []Relationship) { + o.RelationTuples = v +} + +func (o Relationships) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.NextPageToken != nil { + toSerialize["next_page_token"] = o.NextPageToken + } + if o.RelationTuples != nil { + toSerialize["relation_tuples"] = o.RelationTuples + } + return json.Marshal(toSerialize) +} + +type NullableRelationships struct { + value *Relationships + isSet bool +} + +func (v NullableRelationships) Get() *Relationships { + return v.value +} + +func (v *NullableRelationships) Set(val *Relationships) { + v.value = val + v.isSet = true +} + +func (v NullableRelationships) IsSet() bool { + return v.isSet +} + +func (v *NullableRelationships) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableRelationships(val *Relationships) *NullableRelationships { + return &NullableRelationships{value: val, isSet: true} +} + +func (v NullableRelationships) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableRelationships) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_source_position.go b/internal/httpclient/model_source_position.go new file mode 100644 index 0000000..eb763f0 --- /dev/null +++ b/internal/httpclient/model_source_position.go @@ -0,0 +1,150 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// SourcePosition struct for SourcePosition +type SourcePosition struct { + Line *int64 `json:"Line,omitempty"` + Column *int64 `json:"column,omitempty"` +} + +// NewSourcePosition instantiates a new SourcePosition object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSourcePosition() *SourcePosition { + this := SourcePosition{} + return &this +} + +// NewSourcePositionWithDefaults instantiates a new SourcePosition object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSourcePositionWithDefaults() *SourcePosition { + this := SourcePosition{} + return &this +} + +// GetLine returns the Line field value if set, zero value otherwise. +func (o *SourcePosition) GetLine() int64 { + if o == nil || o.Line == nil { + var ret int64 + return ret + } + return *o.Line +} + +// GetLineOk returns a tuple with the Line field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SourcePosition) GetLineOk() (*int64, bool) { + if o == nil || o.Line == nil { + return nil, false + } + return o.Line, true +} + +// HasLine returns a boolean if a field has been set. +func (o *SourcePosition) HasLine() bool { + if o != nil && o.Line != nil { + return true + } + + return false +} + +// SetLine gets a reference to the given int64 and assigns it to the Line field. +func (o *SourcePosition) SetLine(v int64) { + o.Line = &v +} + +// GetColumn returns the Column field value if set, zero value otherwise. +func (o *SourcePosition) GetColumn() int64 { + if o == nil || o.Column == nil { + var ret int64 + return ret + } + return *o.Column +} + +// GetColumnOk returns a tuple with the Column field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SourcePosition) GetColumnOk() (*int64, bool) { + if o == nil || o.Column == nil { + return nil, false + } + return o.Column, true +} + +// HasColumn returns a boolean if a field has been set. +func (o *SourcePosition) HasColumn() bool { + if o != nil && o.Column != nil { + return true + } + + return false +} + +// SetColumn gets a reference to the given int64 and assigns it to the Column field. +func (o *SourcePosition) SetColumn(v int64) { + o.Column = &v +} + +func (o SourcePosition) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Line != nil { + toSerialize["Line"] = o.Line + } + if o.Column != nil { + toSerialize["column"] = o.Column + } + return json.Marshal(toSerialize) +} + +type NullableSourcePosition struct { + value *SourcePosition + isSet bool +} + +func (v NullableSourcePosition) Get() *SourcePosition { + return v.value +} + +func (v *NullableSourcePosition) Set(val *SourcePosition) { + v.value = val + v.isSet = true +} + +func (v NullableSourcePosition) IsSet() bool { + return v.isSet +} + +func (v *NullableSourcePosition) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSourcePosition(val *SourcePosition) *NullableSourcePosition { + return &NullableSourcePosition{value: val, isSet: true} +} + +func (v NullableSourcePosition) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSourcePosition) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_subject_set.go b/internal/httpclient/model_subject_set.go new file mode 100644 index 0000000..7c82c7f --- /dev/null +++ b/internal/httpclient/model_subject_set.go @@ -0,0 +1,168 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// SubjectSet struct for SubjectSet +type SubjectSet struct { + // Namespace of the Subject Set + Namespace string `json:"namespace"` + // Object of the Subject Set + Object string `json:"object"` + // Relation of the Subject Set + Relation string `json:"relation"` +} + +// NewSubjectSet instantiates a new SubjectSet object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSubjectSet(namespace string, object string, relation string) *SubjectSet { + this := SubjectSet{} + this.Namespace = namespace + this.Object = object + this.Relation = relation + return &this +} + +// NewSubjectSetWithDefaults instantiates a new SubjectSet object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSubjectSetWithDefaults() *SubjectSet { + this := SubjectSet{} + return &this +} + +// GetNamespace returns the Namespace field value +func (o *SubjectSet) GetNamespace() string { + if o == nil { + var ret string + return ret + } + + return o.Namespace +} + +// GetNamespaceOk returns a tuple with the Namespace field value +// and a boolean to check if the value has been set. +func (o *SubjectSet) GetNamespaceOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Namespace, true +} + +// SetNamespace sets field value +func (o *SubjectSet) SetNamespace(v string) { + o.Namespace = v +} + +// GetObject returns the Object field value +func (o *SubjectSet) GetObject() string { + if o == nil { + var ret string + return ret + } + + return o.Object +} + +// GetObjectOk returns a tuple with the Object field value +// and a boolean to check if the value has been set. +func (o *SubjectSet) GetObjectOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Object, true +} + +// SetObject sets field value +func (o *SubjectSet) SetObject(v string) { + o.Object = v +} + +// GetRelation returns the Relation field value +func (o *SubjectSet) GetRelation() string { + if o == nil { + var ret string + return ret + } + + return o.Relation +} + +// GetRelationOk returns a tuple with the Relation field value +// and a boolean to check if the value has been set. +func (o *SubjectSet) GetRelationOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Relation, true +} + +// SetRelation sets field value +func (o *SubjectSet) SetRelation(v string) { + o.Relation = v +} + +func (o SubjectSet) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if true { + toSerialize["namespace"] = o.Namespace + } + if true { + toSerialize["object"] = o.Object + } + if true { + toSerialize["relation"] = o.Relation + } + return json.Marshal(toSerialize) +} + +type NullableSubjectSet struct { + value *SubjectSet + isSet bool +} + +func (v NullableSubjectSet) Get() *SubjectSet { + return v.value +} + +func (v *NullableSubjectSet) Set(val *SubjectSet) { + v.value = val + v.isSet = true +} + +func (v NullableSubjectSet) IsSet() bool { + return v.isSet +} + +func (v *NullableSubjectSet) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSubjectSet(val *SubjectSet) *NullableSubjectSet { + return &NullableSubjectSet{value: val, isSet: true} +} + +func (v NullableSubjectSet) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSubjectSet) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/model_version.go b/internal/httpclient/model_version.go new file mode 100644 index 0000000..6fcea01 --- /dev/null +++ b/internal/httpclient/model_version.go @@ -0,0 +1,115 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" +) + +// Version struct for Version +type Version struct { + // Version is the service's version. + Version *string `json:"version,omitempty"` +} + +// NewVersion instantiates a new Version object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewVersion() *Version { + this := Version{} + return &this +} + +// NewVersionWithDefaults instantiates a new Version object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewVersionWithDefaults() *Version { + this := Version{} + return &this +} + +// GetVersion returns the Version field value if set, zero value otherwise. +func (o *Version) GetVersion() string { + if o == nil || o.Version == nil { + var ret string + return ret + } + return *o.Version +} + +// GetVersionOk returns a tuple with the Version field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Version) GetVersionOk() (*string, bool) { + if o == nil || o.Version == nil { + return nil, false + } + return o.Version, true +} + +// HasVersion returns a boolean if a field has been set. +func (o *Version) HasVersion() bool { + if o != nil && o.Version != nil { + return true + } + + return false +} + +// SetVersion gets a reference to the given string and assigns it to the Version field. +func (o *Version) SetVersion(v string) { + o.Version = &v +} + +func (o Version) MarshalJSON() ([]byte, error) { + toSerialize := map[string]interface{}{} + if o.Version != nil { + toSerialize["version"] = o.Version + } + return json.Marshal(toSerialize) +} + +type NullableVersion struct { + value *Version + isSet bool +} + +func (v NullableVersion) Get() *Version { + return v.value +} + +func (v *NullableVersion) Set(val *Version) { + v.value = val + v.isSet = true +} + +func (v NullableVersion) IsSet() bool { + return v.isSet +} + +func (v *NullableVersion) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableVersion(val *Version) *NullableVersion { + return &NullableVersion{value: val, isSet: true} +} + +func (v NullableVersion) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableVersion) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/httpclient/response.go b/internal/httpclient/response.go new file mode 100644 index 0000000..e43e27e --- /dev/null +++ b/internal/httpclient/response.go @@ -0,0 +1,48 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "net/http" +) + +// APIResponse stores the API response returned by the server. +type APIResponse struct { + *http.Response `json:"-"` + Message string `json:"message,omitempty"` + // Operation is the name of the OpenAPI operation. + Operation string `json:"operation,omitempty"` + // RequestURL is the request URL. This value is always available, even if the + // embedded *http.Response is nil. + RequestURL string `json:"url,omitempty"` + // Method is the HTTP method used for the request. This value is always + // available, even if the embedded *http.Response is nil. + Method string `json:"method,omitempty"` + // Payload holds the contents of the response body (which may be nil or empty). + // This is provided here as the raw response.Body() reader will have already + // been drained. + Payload []byte `json:"-"` +} + +// NewAPIResponse returns a new APIResonse object. +func NewAPIResponse(r *http.Response) *APIResponse { + + response := &APIResponse{Response: r} + return response +} + +// NewAPIResponseWithError returns a new APIResponse object with the provided error message. +func NewAPIResponseWithError(errorMessage string) *APIResponse { + + response := &APIResponse{Message: errorMessage} + return response +} diff --git a/internal/httpclient/utils.go b/internal/httpclient/utils.go new file mode 100644 index 0000000..b457140 --- /dev/null +++ b/internal/httpclient/utils.go @@ -0,0 +1,329 @@ +/* + * Ory Keto API + * + * Documentation for all of Ory Keto's REST APIs. gRPC is documented separately. + * + * API version: + * Contact: hi@ory.sh + */ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package client + +import ( + "encoding/json" + "time" +) + +// PtrBool is a helper routine that returns a pointer to given boolean value. +func PtrBool(v bool) *bool { return &v } + +// PtrInt is a helper routine that returns a pointer to given integer value. +func PtrInt(v int) *int { return &v } + +// PtrInt32 is a helper routine that returns a pointer to given integer value. +func PtrInt32(v int32) *int32 { return &v } + +// PtrInt64 is a helper routine that returns a pointer to given integer value. +func PtrInt64(v int64) *int64 { return &v } + +// PtrFloat32 is a helper routine that returns a pointer to given float value. +func PtrFloat32(v float32) *float32 { return &v } + +// PtrFloat64 is a helper routine that returns a pointer to given float value. +func PtrFloat64(v float64) *float64 { return &v } + +// PtrString is a helper routine that returns a pointer to given string value. +func PtrString(v string) *string { return &v } + +// PtrTime is helper routine that returns a pointer to given Time value. +func PtrTime(v time.Time) *time.Time { return &v } + +type NullableBool struct { + value *bool + isSet bool +} + +func (v NullableBool) Get() *bool { + return v.value +} + +func (v *NullableBool) Set(val *bool) { + v.value = val + v.isSet = true +} + +func (v NullableBool) IsSet() bool { + return v.isSet +} + +func (v *NullableBool) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBool(val *bool) *NullableBool { + return &NullableBool{value: val, isSet: true} +} + +func (v NullableBool) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBool) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt struct { + value *int + isSet bool +} + +func (v NullableInt) Get() *int { + return v.value +} + +func (v *NullableInt) Set(val *int) { + v.value = val + v.isSet = true +} + +func (v NullableInt) IsSet() bool { + return v.isSet +} + +func (v *NullableInt) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt(val *int) *NullableInt { + return &NullableInt{value: val, isSet: true} +} + +func (v NullableInt) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt32 struct { + value *int32 + isSet bool +} + +func (v NullableInt32) Get() *int32 { + return v.value +} + +func (v *NullableInt32) Set(val *int32) { + v.value = val + v.isSet = true +} + +func (v NullableInt32) IsSet() bool { + return v.isSet +} + +func (v *NullableInt32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt32(val *int32) *NullableInt32 { + return &NullableInt32{value: val, isSet: true} +} + +func (v NullableInt32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableInt64 struct { + value *int64 + isSet bool +} + +func (v NullableInt64) Get() *int64 { + return v.value +} + +func (v *NullableInt64) Set(val *int64) { + v.value = val + v.isSet = true +} + +func (v NullableInt64) IsSet() bool { + return v.isSet +} + +func (v *NullableInt64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableInt64(val *int64) *NullableInt64 { + return &NullableInt64{value: val, isSet: true} +} + +func (v NullableInt64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableInt64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat32 struct { + value *float32 + isSet bool +} + +func (v NullableFloat32) Get() *float32 { + return v.value +} + +func (v *NullableFloat32) Set(val *float32) { + v.value = val + v.isSet = true +} + +func (v NullableFloat32) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat32) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat32(val *float32) *NullableFloat32 { + return &NullableFloat32{value: val, isSet: true} +} + +func (v NullableFloat32) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat32) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableFloat64 struct { + value *float64 + isSet bool +} + +func (v NullableFloat64) Get() *float64 { + return v.value +} + +func (v *NullableFloat64) Set(val *float64) { + v.value = val + v.isSet = true +} + +func (v NullableFloat64) IsSet() bool { + return v.isSet +} + +func (v *NullableFloat64) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableFloat64(val *float64) *NullableFloat64 { + return &NullableFloat64{value: val, isSet: true} +} + +func (v NullableFloat64) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableFloat64) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableString struct { + value *string + isSet bool +} + +func (v NullableString) Get() *string { + return v.value +} + +func (v *NullableString) Set(val *string) { + v.value = val + v.isSet = true +} + +func (v NullableString) IsSet() bool { + return v.isSet +} + +func (v *NullableString) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableString(val *string) *NullableString { + return &NullableString{value: val, isSet: true} +} + +func (v NullableString) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableString) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +type NullableTime struct { + value *time.Time + isSet bool +} + +func (v NullableTime) Get() *time.Time { + return v.value +} + +func (v *NullableTime) Set(val *time.Time) { + v.value = val + v.isSet = true +} + +func (v NullableTime) IsSet() bool { + return v.isSet +} + +func (v *NullableTime) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableTime(val *time.Time) *NullableTime { + return &NullableTime{value: val, isSet: true} +} + +func (v NullableTime) MarshalJSON() ([]byte, error) { + return v.value.MarshalJSON() +} + +func (v *NullableTime) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/internal/namespace/ast/ast_definitions.go b/internal/namespace/ast/ast_definitions.go new file mode 100644 index 0000000..819d092 --- /dev/null +++ b/internal/namespace/ast/ast_definitions.go @@ -0,0 +1,73 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import "encoding/json" + +type ( + Relation struct { + Name string `json:"name"` + Types []RelationType `json:"types,omitempty"` + SubjectSetRewrite *SubjectSetRewrite `json:"rewrite,omitempty"` + } + + RelationType struct { + Namespace string `json:"namespace"` + Relation string `json:"relation,omitempty"` // optional + } + + SubjectSetRewrite struct { + Operation Operator `json:"operator"` + Children Children `json:"children"` + } + + Children = []Child + + // Child are all possible types of subject-set rewrites. + Child interface { + // AsRewrite returns the child as a subject-set rewrite, as relations + // require a top-level rewrite, even if just one child was parsed. + AsRewrite() *SubjectSetRewrite + // EstimatedSize approximates the memory in bytes retained by the node. + EstimatedSize() int64 + } + + ComputedSubjectSet struct { + Relation string `json:"relation"` + } + + TupleToSubjectSet struct { + Relation string `json:"relation"` + ComputedSubjectSetRelation string `json:"computed_subject_set_relation"` + } + + // InvertResult inverts the check result of the child. + InvertResult struct { + Child Child `json:"inverted"` + } +) + +type Operator int + +//go:generate stringer -type=Operator -linecomment +const ( + OperatorOr Operator = iota // or + OperatorAnd // and +) + +func (i Operator) MarshalJSON() ([]byte, error) { + return json.Marshal(i.String()) + +} + +func (r *SubjectSetRewrite) AsRewrite() *SubjectSetRewrite { return r } +func (c *ComputedSubjectSet) AsRewrite() *SubjectSetRewrite { + return &SubjectSetRewrite{Children: []Child{c}} +} +func (t *TupleToSubjectSet) AsRewrite() *SubjectSetRewrite { + return &SubjectSetRewrite{Children: []Child{t}} +} +func (i *InvertResult) AsRewrite() *SubjectSetRewrite { + return &SubjectSetRewrite{Children: []Child{i}} +} diff --git a/internal/namespace/ast/operator_string.go b/internal/namespace/ast/operator_string.go new file mode 100644 index 0000000..74f212c --- /dev/null +++ b/internal/namespace/ast/operator_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=Operator -linecomment"; DO NOT EDIT. + +package ast + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[OperatorOr-0] + _ = x[OperatorAnd-1] +} + +const _Operator_name = "orand" + +var _Operator_index = [...]uint8{0, 2, 5} + +func (i Operator) String() string { + if i < 0 || i >= Operator(len(_Operator_index)-1) { + return "Operator(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Operator_name[_Operator_index[i]:_Operator_index[i+1]] +} diff --git a/internal/namespace/ast/size.go b/internal/namespace/ast/size.go new file mode 100644 index 0000000..7d87467 --- /dev/null +++ b/internal/namespace/ast/size.go @@ -0,0 +1,54 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ast + +import "unsafe" + +// EstimatedSize approximates the memory in bytes retained by the relation. +func (r *Relation) EstimatedSize() int64 { + s := int64(unsafe.Sizeof(*r)) + int64(len(r.Name)) + for _, t := range r.Types { + s += int64(unsafe.Sizeof(t)) + int64(len(t.Namespace)+len(t.Relation)) + } + return s + r.SubjectSetRewrite.EstimatedSize() +} + +func (r *SubjectSetRewrite) EstimatedSize() int64 { + if r == nil { + return 0 + } + s := int64(unsafe.Sizeof(*r)) + for _, c := range r.Children { + s += int64(unsafe.Sizeof(c)) + childSize(c) + } + return s +} + +func (c *ComputedSubjectSet) EstimatedSize() int64 { + if c == nil { + return 0 + } + return int64(unsafe.Sizeof(*c)) + int64(len(c.Relation)) +} + +func (t *TupleToSubjectSet) EstimatedSize() int64 { + if t == nil { + return 0 + } + return int64(unsafe.Sizeof(*t)) + int64(len(t.Relation)+len(t.ComputedSubjectSetRelation)) +} + +func (i *InvertResult) EstimatedSize() int64 { + if i == nil { + return 0 + } + return int64(unsafe.Sizeof(*i)) + childSize(i.Child) +} + +func childSize(c Child) int64 { + if c == nil { + return 0 + } + return c.EstimatedSize() +} diff --git a/internal/namespace/ast/size_test.go b/internal/namespace/ast/size_test.go new file mode 100644 index 0000000..332d592 --- /dev/null +++ b/internal/namespace/ast/size_test.go @@ -0,0 +1,89 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ast_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/namespace/ast" +) + +// The expected values are exact struct sizes on 64-bit platforms: +// Relation 48, RelationType 32, SubjectSetRewrite 32, ComputedSubjectSet 16, +// TupleToSubjectSet 32, InvertResult 16, interface header 16, plus string +// lengths. If this test breaks, an AST type changed: update the size +// accounting in size.go together with these numbers. +func TestRelationEstimatedSize(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + relation ast.Relation + expected int64 + }{ + { + name: "empty relation", + relation: ast.Relation{}, + expected: 48, + }, + { + name: "relation with name and types", + relation: ast.Relation{ + Name: "viewers", + Types: []ast.RelationType{ + {Namespace: "User"}, + {Namespace: "Group", Relation: "members"}, + }, + }, + expected: 48 + 7 + // struct + "viewers" + 32 + 4 + // type + "User" + 32 + 5 + 7, // type + "Group" + "members" + }, + { + name: "relation with a rewrite of every node type", + relation: ast.Relation{ + Name: "view", + SubjectSetRewrite: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "editors"}, + &ast.TupleToSubjectSet{Relation: "parents", ComputedSubjectSetRelation: "view"}, + &ast.InvertResult{Child: &ast.ComputedSubjectSet{Relation: "banned"}}, + &ast.SubjectSetRewrite{Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "owners"}, + }}, + }, + }, + }, + expected: 48 + 4 + // struct + "view" + 32 + // rewrite + 16 + 16 + 7 + // header + computed + "editors" + 16 + 32 + 7 + 4 + // header + tuple + "parents" + "view" + 16 + 16 + 16 + 6 + // header + invert + computed + "banned" + 16 + 32 + 16 + 16 + 6, // header + nested rewrite + header + computed + "owners" + }, + { + name: "nil children are safe and cost nothing", + relation: ast.Relation{ + SubjectSetRewrite: &ast.SubjectSetRewrite{ + Children: ast.Children{ + nil, + (*ast.SubjectSetRewrite)(nil), + (*ast.ComputedSubjectSet)(nil), + (*ast.TupleToSubjectSet)(nil), + (*ast.InvertResult)(nil), + &ast.InvertResult{}, + }, + }, + }, + expected: 48 + 32 + 6*16 + 16, // struct + rewrite + 6 headers + empty invert + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tc.expected, tc.relation.EstimatedSize()) + }) + } +} diff --git a/internal/namespace/definitions.go b/internal/namespace/definitions.go new file mode 100644 index 0000000..f24a6f2 --- /dev/null +++ b/internal/namespace/definitions.go @@ -0,0 +1,73 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import ( + "context" + "encoding/json" + + "github.com/ory/herodot" + + "github.com/ory/keto/internal/namespace/ast" +) + +type ( + Namespace struct { + // Deprecated: Only use the Name instead. + ID int32 `json:"id" db:"-" toml:"id"` + // The unique name of the namespace. + Name string `json:"name" db:"-" toml:"name"` + Config json.RawMessage `json:"config,omitempty" db:"-" toml:"config,omitempty"` + + Relations []ast.Relation `json:"-" db:"-"` + } + Manager interface { + GetNamespaceByName(ctx context.Context, name string) (*Namespace, error) + // Deprecated: Use GetNamespaceByName instead. + GetNamespaceByConfigID(ctx context.Context, id int32) (*Namespace, error) + Namespaces(ctx context.Context) ([]*Namespace, error) + ShouldReload(newValue interface{}) bool + } + ManagerProvider interface { + NamespaceManager() (Manager, error) + } +) + +func (ns *Namespace) FindRelation(name string) *ast.Relation { + if ns == nil || len(ns.Relations) == 0 { + return nil + } + for _, rel := range ns.Relations { + if rel.Name == name { + return &rel + } + } + return nil +} + +func ASTRelationFor(ctx context.Context, m Manager, namespace, relation string) (*ast.Relation, error) { + // Special case: If the relationTuple's relation is empty, then it is not an + // error that the relation was not found. + if relation == "" { + return nil, nil + } + ns, err := m.GetNamespaceByName(ctx, namespace) + if err != nil { + // On an unknown namespace the answer should be "not allowed", not "not + // found". Therefore, we don't return the error here. + return nil, nil + } + + // Special case: If Relations is empty, then there is no namespace + // configuration, and it is not an error that the relation was not found. + if len(ns.Relations) == 0 { + return nil, nil + } + + rel := ns.FindRelation(relation) + if rel == nil { + return nil, herodot.ErrBadRequest().WithReasonf("relation %q does not exist", relation) + } + return rel, nil +} diff --git a/internal/namespace/definitions_test.go b/internal/namespace/definitions_test.go new file mode 100644 index 0000000..16d46f0 --- /dev/null +++ b/internal/namespace/definitions_test.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace_test + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/ory/herodot" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" +) + +func TestASTRelationFor(t *testing.T) { + ctx := context.Background() + nm := config.NewMemoryNamespaceManager(&namespace.Namespace{ + Name: "test", + Relations: []ast.Relation{{Name: "test"}}, + }) + + rel, err := namespace.ASTRelationFor(ctx, nm, "test", "test") + require.NoError(t, err) + assert.Equal(t, "test", rel.Name) + + _, err = namespace.ASTRelationFor(ctx, nm, "test", "unknown") + herr, ok := errors.AsType[*herodot.DefaultError](err) + require.True(t, ok) + assert.Equal(t, http.StatusBadRequest, herr.CodeField) + + rel, err = namespace.ASTRelationFor(ctx, nm, "unknown", "") + require.NoError(t, err) + assert.Nil(t, rel) +} diff --git a/internal/namespace/namespacehandler/handler.go b/internal/namespace/namespacehandler/handler.go new file mode 100644 index 0000000..ca50925 --- /dev/null +++ b/internal/namespace/namespacehandler/handler.go @@ -0,0 +1,103 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespacehandler + +import ( + "context" + "net/http" + + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/herodot" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/x" +) + +type ( + handlerDeps interface { + logrusx.Provider + httpx.WriterProvider + config.Provider + x.HandlerOptionsProvider + } + Handler struct { + relationtuplesconnect.UnimplementedNamespacesServiceHandler + d handlerDeps + } +) + +const ( + RouteBase = "/namespaces" +) + +func New(d handlerDeps) *Handler { return &Handler{d: d} } + +func (h *Handler) RegisterReadRoutes(r *httprouterx.RouterPublic) { + r.GET(RouteBase, h.getNamespaces) + + listNamespacesHandler := connect.NewUnaryHandler( + relationtuplesconnect.NamespacesServiceListNamespacesProcedure, + h.ListNamespaces, + connect.WithSchema(rts.File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto. + Services().ByName("NamespacesService"). + Methods().ByName("ListNamespaces")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.NamespacesServiceListNamespacesProcedure, listNamespacesHandler) +} + +func (h *Handler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{rts.File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto} +} + +func (h *Handler) ListNamespaces(ctx context.Context, _ *connect.Request[rts.ListNamespacesRequest]) (*connect.Response[rts.ListNamespacesResponse], error) { + m, err := h.d.Config(ctx).NamespaceManager() + if err != nil { + h.d.Logger().WithError(err).Errorf("could not get namespace manager") + return nil, herodot.ErrInternalServerError() + } + namespaces, err := m.Namespaces(ctx) + if err != nil { + h.d.Logger().WithError(err).Errorf("could not get namespaces") + return nil, herodot.ErrInternalServerError() + } + apiNamespaces := make([]*rts.Namespace, len(namespaces)) + for i, n := range namespaces { + apiNamespaces[i] = &rts.Namespace{Name: n.Name} + } + return connect.NewResponse(&rts.ListNamespacesResponse{Namespaces: apiNamespaces}), nil +} + +// swagger:route GET /namespaces relationship listRelationshipNamespaces +// +// # Query namespaces +// +// Get all namespaces +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: relationshipNamespaces +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-low +func (h *Handler) getNamespaces(w http.ResponseWriter, r *http.Request) { + res, err := h.ListNamespaces(r.Context(), nil) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + h.d.Writer().Write(w, r, res.Msg) +} diff --git a/internal/namespace/size.go b/internal/namespace/size.go new file mode 100644 index 0000000..cbcb9ea --- /dev/null +++ b/internal/namespace/size.go @@ -0,0 +1,21 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace + +import "unsafe" + +// EstimatedSize approximates the memory in bytes retained by the namespaces. +func EstimatedSize(nn []*Namespace) int64 { + var s int64 + for _, n := range nn { + if n == nil { + continue + } + s += int64(unsafe.Sizeof(n)) + int64(unsafe.Sizeof(*n)) + int64(len(n.Name)+len(n.Config)) + for i := range n.Relations { + s += n.Relations[i].EstimatedSize() + } + } + return s +} diff --git a/internal/namespace/size_test.go b/internal/namespace/size_test.go new file mode 100644 index 0000000..afea6a3 --- /dev/null +++ b/internal/namespace/size_test.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package namespace_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" +) + +func TestEstimatedSize(t *testing.T) { + t.Parallel() + + require.Zero(t, namespace.EstimatedSize(nil)) + require.Zero(t, namespace.EstimatedSize([]*namespace.Namespace{})) + + nn := []*namespace.Namespace{ + {Name: "Document", Config: json.RawMessage(`{}`)}, + nil, // Must not panic. + {Name: "User", Relations: []ast.Relation{{Name: "manager"}}}, + } + require.EqualValues(t, + 8+72+8+2+ // pointer + Namespace + "Document" + config + 8+72+4+ // pointer + Namespace + "User" + 48+7, // relation + "manager" + namespace.EstimatedSize(nn)) +} diff --git a/internal/persistence/definitions.go b/internal/persistence/definitions.go new file mode 100644 index 0000000..a35a662 --- /dev/null +++ b/internal/persistence/definitions.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package persistence + +import ( + "context" + "errors" + + "github.com/gofrs/uuid" + "github.com/ory/x/popx" + + "github.com/ory/pop/v6" + + "github.com/ory/keto/internal/relationtuple" +) + +type ( + Persister interface { + relationtuple.Manager + relationtuple.MappingManager + + Connection(ctx context.Context) *pop.Connection + NetworkID(ctx context.Context) uuid.UUID + SetNetwork(nid uuid.UUID) + } + Migrator interface { + MigrationBox(ctx context.Context, opts ...popx.MigrationBoxOption) (*popx.MigrationBox, error) + MigrateUp(ctx context.Context) error + MigrateDown(ctx context.Context) error + } + Provider interface { + Persister() Persister + } +) + +var ( + ErrMalformedPageToken = errors.New("malformed page token") + ErrNetworkMigrationsMissing = errors.New("networkx migrations are not yet applied") +) diff --git a/internal/persistence/sql/full_test.go b/internal/persistence/sql/full_test.go new file mode 100644 index 0000000..ff4c108 --- /dev/null +++ b/internal/persistence/sql/full_test.go @@ -0,0 +1,62 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + "context" + "fmt" + "testing" + + "github.com/ory/x/networkx" + "github.com/sirupsen/logrus/hooks/test" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/persistence" + "github.com/ory/keto/internal/persistence/sql" + relationtupletest "github.com/ory/keto/internal/relationtuple/test" + "github.com/ory/keto/internal/x/dbx" +) + +func TestPersister(t *testing.T) { + t.Parallel() + + setup := func(t *testing.T, dsn *dbx.DsnT) (p persistence.Persister, r *driver.RegistryDefault, hook *test.Hook) { + r = driver.NewTestRegistry(t, dsn) + p = r.Persister() + require.NotNil(t, p) + require.NoError(t, r.MigrateUp(context.Background())) + return + } + + for _, dsn := range dbx.GetDSNs(t) { + t.Run(fmt.Sprintf("dsn=%s", dsn.Name), func(t *testing.T) { + t.Parallel() + + t.Run("relationtuple.ManagerTest", func(t *testing.T) { + p, _, _ := setup(t, dsn) + + relationtupletest.ManagerTest(t, p) + }) + + t.Run("relationtuple.IsolationTest", func(t *testing.T) { + p0, r, _ := setup(t, dsn) + n1 := networkx.NewNetwork() + conn, err := r.PopConnection(context.Background()) + require.NoError(t, err) + require.NoError(t, conn.Create(n1)) + p1, err := sql.NewPersister(context.Background(), r, n1.ID) + require.NoError(t, err) + + // same registry, but different persisters only differing in the network ID + relationtupletest.IsolationTest(t, p0, p1) + }) + + t.Run("relationtuple.UUIDMappingManagerTest", func(t *testing.T) { + p, _, _ := setup(t, dsn) + relationtupletest.MappingManagerTest(t, p) + }) + }) + } +} diff --git a/internal/persistence/sql/migrations/migratest/migration_test.go b/internal/persistence/sql/migrations/migratest/migration_test.go new file mode 100644 index 0000000..fb89f78 --- /dev/null +++ b/internal/persistence/sql/migrations/migratest/migration_test.go @@ -0,0 +1,275 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migratest + +import ( + "context" + stdSql "database/sql" + "os" + "strconv" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + "github.com/ory/x/fsx" + "github.com/ory/x/logrusx" + "github.com/ory/x/networkx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/persistence/sql" + "github.com/ory/keto/internal/persistence/sql/migrations/uuidmapping" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/x/dbx" + "github.com/ory/keto/ketoapi" +) + +func TestMigrations(t *testing.T) { + t.Parallel() + const debugOnDisk = false + + for _, db := range dbx.GetDSNs(t) { + t.Run("dsn="+db.Name, func(t *testing.T) { + t.Parallel() + + db.MigrateUp, db.MigrateDown = false, false + + var conn *pop.Connection + var err error + conn, err = pop.NewConnection(&pop.ConnectionDetails{URL: db.Conn}) + require.NoError(t, err) + for range 120 { + require.NoError(t, conn.Open()) + if err := dbx.Ping(conn); err == nil { + break + } + time.Sleep(time.Second) + } + require.NoError(t, dbx.Ping(conn)) + t.Cleanup(func() { require.NoError(t, conn.Close()) }) + + namespaces := []*namespace.Namespace{ + {ID: 1, Name: "foo"}, + {ID: 2, Name: "uuid_test"}, + } + nm := config.NewMemoryNamespaceManager(namespaces...) + tm, err := popx.NewMigrationBox( + fsx.Merge(sql.Migrations, networkx.Migrations), + conn, logrusx.New("", "", logrusx.ForceLevel(logrus.DebugLevel)), + popx.WithGoMigrations(uuidmapping.Migrations(nm)), + popx.WithTestdata(t, os.DirFS("./testdata")), + popx.WithoutGoldenDatabase(), + ) + require.NoError(t, err) + + // cleanup first + require.NoError(t, tm.Down(t.Context(), -1)) + + t.Run("suite=up", func(t *testing.T) { + if err := tm.Up(t.Context()); err != nil { + t.Log("migrations failed:", err) + logMigrationStatus(t, tm) + t.FailNow() + } + }) + + reg := driver.NewTestRegistry(t, db, driver.WithNamespaces(namespaces)) + p, err := sql.NewPersister(t.Context(), reg, uuid.Must(uuid.FromString("77fdc5e0-2260-49da-8aae-c36ba255d05b"))) + require.NoError(t, err) + + t.Run("suite=fixtures", func(t *testing.T) { + t.Run("table=legacy namespaces", func(t *testing.T) { + // as they are legacy, we expect them to be actually dropped + assert.ErrorIs(t, sqlcon.HandleError(conn.RawQuery( + "SELECT * FROM keto_namespace", + ).Exec()), sqlcon.ErrNoSuchTable()) + }) + + t.Run("table=relationships", func(t *testing.T) { + actualRts, next, err := p.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &namespaces[0].Name}) + require.NoError(t, err) + assert.True(t, next.IsLast()) + t.Log("actual rts:", actualRts) + + expectedRts := []*ketoapi.RelationTuple{ + { + Namespace: "foo", + Object: "object", + Relation: "relation", + SubjectID: new("user"), + }, + { + Namespace: "foo", + Object: "object", + Relation: "relation", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: "foo", + Object: "s_object", + Relation: "s_relation", + }, + }, + } + + // The relationship tuples in the db have a UUID mapping, so + // we need to convert our expectations to that. + expectedUUID, err := reg.Mapper().FromTuple(t.Context(), expectedRts...) + require.NoError(t, err) + assert.ElementsMatch(t, expectedUUID, actualRts) + logMigrationStatus(t, tm) + }) + }) + + t.Run("suite=uuid_migrations", func(t *testing.T) { + t.Run("correct types", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), time.Minute) + defer cancel() + require.NoError(t, tm.Down(ctx, -1)) + + // Migrate up to (including) "drop old non-uuid table" + migrateUpTo(t, tm, "20220513200600000001") + t.Log("status after up migration") + logMigrationStatus(t, tm) + + // Assert that relationtuples have UUIDs + tuples, _, err := p.GetRelationTuples(ctx, &relationtuple.RelationQuery{Namespace: &namespaces[1].Name}) + require.NoError(t, err) + assert.NotZero(t, tuples[0].Subject.(*relationtuple.SubjectID).ID) + assert.NotZero(t, tuples[0].Object) + + // Migrate down to before "migrate-strings-to-uuids" + migrateDownTo(t, tm, "20220513200300000000") + t.Log("status after down migration") + logMigrationStatus(t, tm) + + // Assert that relationtuples have strings + var oldRTs []*tuplesBeforeUUID + require.NoError(t, p.Connection(ctx). + Select("subject_id", "object"). + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + Where("namespace_id = ?", namespaces[1].ID). + All(&oldRTs)) + assert.Equalf(t, "user", oldRTs[0].SubjectID.String, "%+v", oldRTs[0]) + assert.Equal(t, "object", oldRTs[0].Object) + }) + + t.Run("paginates", func(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 2*time.Minute) + defer cancel() + require.NoError(t, tm.Down(ctx, -1)) + + // migrate up to before all UUID migrations + migrateUpTo(t, tm, "20220512151000000000") + t.Log("status after up migration") + logMigrationStatus(t, tm) + + oldRTs := make([]tuplesBeforeUUID, 2000) + expected := make([]*ketoapi.RelationTuple, len(oldRTs)) + for i := range oldRTs { + oldRTs[i] = tuplesBeforeUUID{ + NetworkID: p.NetworkID(ctx), + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + NamespaceID: namespaces[1].ID, + Object: "object-" + strconv.Itoa(i), + Relation: "pagination-works", + SubjectID: stdSql.NullString{String: "subject-" + strconv.Itoa(i), Valid: true}, + CommitTime: time.Now(), + } + expected[i] = &ketoapi.RelationTuple{ + Namespace: namespaces[1].Name, + Object: oldRTs[i].Object, + Relation: "pagination-works", + SubjectID: new(oldRTs[i].SubjectID.String), + } + } + require.NoError(t, p.Connection(ctx).Create(oldRTs)) + require.NoError(t, tm.Up(ctx)) + + newRTs := make([]*relationtuple.RelationTuple, 0, len(oldRTs)) + for nextPage, _ := keysetpagination.NewPaginator(); !nextPage.IsLast(); { + var rts []*relationtuple.RelationTuple + rts, nextPage, err = p.GetRelationTuples(ctx, &relationtuple.RelationQuery{Relation: new("pagination-works")}, nextPage.ToOptions()...) + require.NoError(t, err) + newRTs = append(newRTs, rts...) + } + assert.Len(t, newRTs, len(oldRTs)) + actual, err := reg.Mapper().ToTuple(ctx, newRTs...) + require.NoError(t, err) + assert.ElementsMatch(t, expected, actual) + }) + }) + + t.Run("suite=down", func(t *testing.T) { + if debugOnDisk && db.Name == "sqlite" { + t.SkipNow() + } + require.NoError(t, tm.Down(t.Context(), -1)) + }) + }) + } +} + +// migrateUpTo migrates up to the specified version (inclusive) +func migrateUpTo(t *testing.T, tm *popx.MigrationBox, version string) { + statuses, err := tm.Status(context.Background()) + require.NoError(t, err) + + for i, status := range statuses { + if status.Version == version { + _, err = tm.UpTo(context.Background(), i+1) + require.NoError(t, err) + return + } + } + t.Fatal("could not find ", version) +} + +// migrateDownTo migrates down to the specified version (exclusive) +func migrateDownTo(t *testing.T, tm *popx.MigrationBox, version string) { + statuses, err := tm.Status(context.Background()) + require.NoError(t, err) + + for i, status := range statuses { + if status.Version == version { + require.NoError(t, tm.Down(context.Background(), len(statuses)-i)) + return + } + } + t.Fatal("could not find ", version) +} + +func logMigrationStatus(t *testing.T, m *popx.MigrationBox) { + t.Helper() + + status, err := m.Status(context.Background()) + require.NoError(t, err) + t.Logf("status: %+v", status) +} + +type tuplesBeforeUUID struct { + ID uuid.UUID `db:"shard_id"` + NetworkID uuid.UUID `db:"nid"` + NamespaceID int32 `db:"namespace_id"` + Object string `db:"object"` + Relation string `db:"relation"` + SubjectID stdSql.NullString `db:"subject_id"` + SubjectSetNamespaceID stdSql.NullInt32 `db:"subject_set_namespace_id"` + SubjectSetObject stdSql.NullString `db:"subject_set_object"` + SubjectSetRelation stdSql.NullString `db:"subject_set_relation"` + CommitTime time.Time `db:"commit_time"` +} + +func (tuplesBeforeUUID) TableName(_ context.Context) string { + return "keto_relation_tuples" +} diff --git a/internal/persistence/sql/migrations/migratest/testdata/20150100000001_testdata.sql b/internal/persistence/sql/migrations/migratest/testdata/20150100000001_testdata.sql new file mode 100644 index 0000000..d13521e --- /dev/null +++ b/internal/persistence/sql/migrations/migratest/testdata/20150100000001_testdata.sql @@ -0,0 +1 @@ +INSERT INTO networks (id, created_at, updated_at) VALUES ('77fdc5e0-2260-49da-8aae-c36ba255d05b', '2015-01-01 00:00:00', '2015-01-01 00:00:00'); diff --git a/internal/persistence/sql/migrations/migratest/testdata/20201110175414_testdata.sql b/internal/persistence/sql/migrations/migratest/testdata/20201110175414_testdata.sql new file mode 100644 index 0000000..2ad3896 --- /dev/null +++ b/internal/persistence/sql/migrations/migratest/testdata/20201110175414_testdata.sql @@ -0,0 +1 @@ +INSERT INTO keto_namespace (id, schema_version) VALUES (1, 1); diff --git a/internal/persistence/sql/migrations/migratest/testdata/20210623162417_testdata.sql b/internal/persistence/sql/migrations/migratest/testdata/20210623162417_testdata.sql new file mode 100644 index 0000000..c19082e --- /dev/null +++ b/internal/persistence/sql/migrations/migratest/testdata/20210623162417_testdata.sql @@ -0,0 +1,14 @@ +INSERT INTO keto_relation_tuples (shard_id, nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, + subject_set_object, subject_set_relation, commit_time) +VALUES ('7a9d6df4-3b60-4cae-996e-d15d78b0fc36', '77fdc5e0-2260-49da-8aae-c36ba255d05b', 1, 'object', 'relation', 'user', + NULL, NULL, NULL, '2021-09-30 15:50:52'); + +INSERT INTO keto_relation_tuples (shard_id, nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, + subject_set_object, subject_set_relation, commit_time) +VALUES ('1457d171-4473-4c77-bd88-0809529c599f', '77fdc5e0-2260-49da-8aae-c36ba255d05b', 1, 'object', 'relation', NULL, + 1, 's_object', 's_relation', '2021-09-30 15:50:52'); + +INSERT INTO keto_relation_tuples (shard_id, nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, + subject_set_object, subject_set_relation, commit_time) +VALUES ('7a9d6df4-3b60-4cae-996e-d15d78b0fc35', '77fdc5e0-2260-49da-8aae-c36ba255d05b', 2, 'object', 'relation', 'user', + NULL, NULL, NULL, '2021-09-30 15:50:52'); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/migratest/testdata/20210914134624_testdata.sql b/internal/persistence/sql/migrations/migratest/testdata/20210914134624_testdata.sql new file mode 100644 index 0000000..49c3a0c --- /dev/null +++ b/internal/persistence/sql/migrations/migratest/testdata/20210914134624_testdata.sql @@ -0,0 +1 @@ +-- DROP TABLE statement only, nothing to insert diff --git a/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.down.sql b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.down.sql new file mode 100644 index 0000000..cc7e55c --- /dev/null +++ b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.down.sql @@ -0,0 +1,5 @@ +DROP INDEX {{ identifier .Parameters.tableName }}_user_set_idx; + +DROP INDEX {{ identifier .Parameters.tableName }}_object_idx; + +DROP TABLE {{ identifier .Parameters.tableName }}; diff --git a/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.mysql.down.sql b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.mysql.down.sql new file mode 100644 index 0000000..62b69a6 --- /dev/null +++ b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.mysql.down.sql @@ -0,0 +1,5 @@ +DROP INDEX {{ identifier .Parameters.tableName }}_user_set_idx ON {{ identifier .Parameters.tableName }}; + +DROP INDEX {{ identifier .Parameters.tableName }}_object_idx ON {{ identifier .Parameters.tableName }}; + +DROP TABLE {{ identifier .Parameters.tableName }}; diff --git a/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.up.sql b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.up.sql new file mode 100644 index 0000000..b9642b7 --- /dev/null +++ b/internal/persistence/sql/migrations/namespace_migrations/20210128140414_namespace.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE {{ identifier .Parameters.tableName }} +( + shard_id varchar(64), + object varchar(64), + relation varchar(64), + subject varchar(256), /* can be or */ + commit_time timestamp, + + PRIMARY KEY (shard_id, object, relation, subject, commit_time) +); + +CREATE INDEX {{ identifier .Parameters.tableName }}_object_idx ON {{ identifier .Parameters.tableName }} (object); + +CREATE INDEX {{ identifier .Parameters.tableName }}_user_set_idx ON {{ identifier .Parameters.tableName }} (object, relation); diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..e6c51f3 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.cockroach.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE keto_namespace +( + id INTEGER PRIMARY KEY, + schema_version INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.mysql.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.mysql.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.mysql.up.sql new file mode 100644 index 0000000..e6c51f3 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE keto_namespace +( + id INTEGER PRIMARY KEY, + schema_version INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.postgres.up.sql new file mode 100644 index 0000000..e6c51f3 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.postgres.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE keto_namespace +( + id INTEGER PRIMARY KEY, + schema_version INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..e6c51f3 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000000_relationtuple.sqlite3.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE keto_namespace +( + id INTEGER PRIMARY KEY, + schema_version INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..ab89a03 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_namespace; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.mysql.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.mysql.down.sql new file mode 100644 index 0000000..ab89a03 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_namespace; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.mysql.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.postgres.down.sql new file mode 100644 index 0000000..ab89a03 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_namespace; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..ab89a03 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_namespace; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20201110175414000001_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..d78087e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.cockroach.up.sql @@ -0,0 +1,25 @@ +-- we can not fizz this migration because there is no check constraint support in fizz +-- and it gets cumbersome with sqlite; having a working SQL version is actually way easier +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.mysql.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.mysql.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.mysql.up.sql new file mode 100644 index 0000000..cc7aae4 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.mysql.up.sql @@ -0,0 +1,25 @@ +-- we can not fizz this migration because there is no check constraint support in fizz +-- and it gets cumbersome with sqlite; having a working SQL version is actually way easier +CREATE TABLE keto_relation_tuples +( + shard_id char(36) NOT NULL, + nid char(36) NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.postgres.up.sql new file mode 100644 index 0000000..d78087e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.postgres.up.sql @@ -0,0 +1,25 @@ +-- we can not fizz this migration because there is no check constraint support in fizz +-- and it gets cumbersome with sqlite; having a working SQL version is actually way easier +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..d78087e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000000_relationtuple.sqlite3.up.sql @@ -0,0 +1,25 @@ +-- we can not fizz this migration because there is no check constraint support in fizz +-- and it gets cumbersome with sqlite; having a working SQL version is actually way easier +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..04ba855 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..adebfe6 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.cockroach.up.sql @@ -0,0 +1,6 @@ + +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id) WHERE subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.down.sql new file mode 100644 index 0000000..04ba855 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.up.sql new file mode 100644 index 0000000..df1bcc6 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.mysql.up.sql @@ -0,0 +1,11 @@ + +-- mysql has no partial indexes so we can only use the full one +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + commit_time); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.down.sql new file mode 100644 index 0000000..04ba855 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.up.sql new file mode 100644 index 0000000..adebfe6 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.postgres.up.sql @@ -0,0 +1,6 @@ + +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id) WHERE subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..04ba855 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..adebfe6 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000001_relationtuple.sqlite3.up.sql @@ -0,0 +1,6 @@ + +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id) WHERE subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..5547388 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.cockroach.up.sql @@ -0,0 +1,8 @@ + +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_set_namespace_id, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.postgres.up.sql new file mode 100644 index 0000000..5547388 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.postgres.up.sql @@ -0,0 +1,8 @@ + +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_set_namespace_id, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..5547388 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000002_relationtuple.sqlite3.up.sql @@ -0,0 +1,8 @@ + +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_set_namespace_id, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.cockroach.up.sql new file mode 100644 index 0000000..885fe6a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.cockroach.up.sql @@ -0,0 +1,10 @@ + +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + commit_time); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.postgres.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.postgres.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.postgres.up.sql new file mode 100644 index 0000000..885fe6a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.postgres.up.sql @@ -0,0 +1,10 @@ + +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + commit_time); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.sqlite3.up.sql new file mode 100644 index 0000000..885fe6a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210623162417000003_relationtuple.sqlite3.up.sql @@ -0,0 +1,10 @@ + +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + commit_time); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.down.sql new file mode 100644 index 0000000..fde3c6a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.down.sql @@ -0,0 +1,5 @@ +CREATE TABLE "keto_namespace" ( +"id" SERIAL NOT NULL, +PRIMARY KEY("id"), +"schema_version" int NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.up.sql new file mode 100644 index 0000000..aa17820 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.cockroach.up.sql @@ -0,0 +1 @@ +DROP TABLE "keto_namespace"; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.down.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.down.sql new file mode 100644 index 0000000..8aef7cf --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.down.sql @@ -0,0 +1,5 @@ +CREATE TABLE `keto_namespace` ( +`id` INTEGER NOT NULL AUTO_INCREMENT, +PRIMARY KEY(`id`), +`schema_version` INTEGER NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.up.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.up.sql new file mode 100644 index 0000000..5b1def1 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.mysql.up.sql @@ -0,0 +1 @@ +DROP TABLE `keto_namespace`; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.down.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.down.sql new file mode 100644 index 0000000..fde3c6a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.down.sql @@ -0,0 +1,5 @@ +CREATE TABLE "keto_namespace" ( +"id" SERIAL NOT NULL, +PRIMARY KEY("id"), +"schema_version" int NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.up.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.up.sql new file mode 100644 index 0000000..aa17820 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.postgres.up.sql @@ -0,0 +1 @@ +DROP TABLE "keto_namespace"; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.down.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.down.sql new file mode 100644 index 0000000..30f9414 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.down.sql @@ -0,0 +1,4 @@ +CREATE TABLE "keto_namespace" ( +"id" INTEGER PRIMARY KEY AUTOINCREMENT, +"schema_version" INTEGER NOT NULL +); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.up.sql b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.up.sql new file mode 100644 index 0000000..aa17820 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20210914134624000000_legacy-cleanup.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "keto_namespace"; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.down.sql b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.down.sql new file mode 100644 index 0000000..afbcf54 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.down.sql @@ -0,0 +1 @@ +ALTER TABLE keto_relation_tuples DROP CONSTRAINT keto_relation_tuples_nid_fk; diff --git a/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.down.sql b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.down.sql new file mode 100644 index 0000000..e8faad3 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.down.sql @@ -0,0 +1,29 @@ +CREATE TABLE keto_relation_tuples_no_fk +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); + +INSERT INTO keto_relation_tuples_no_fk (shard_id, nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, subject_set_object, subject_set_relation, commit_time) SELECT * FROM keto_relation_tuples; + +DROP TABLE keto_relation_tuples; + +ALTER TABLE keto_relation_tuples_no_fk RENAME TO keto_relation_tuples; diff --git a/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.up.sql b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.up.sql new file mode 100644 index 0000000..c864d22 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.sqlite.up.sql @@ -0,0 +1,59 @@ +-- These tuples can't be retrieved over any API (and also not inserted by any API) because we always add a `WHERE nid = ?` clause to the queries. +-- This DELETE statement is here to ensure that we don't run into problems where the `nid` column is not properly set. Should not happen if only the official API is used. +DELETE FROM keto_relation_tuples WHERE nid NOT IN (SELECT nid FROM networks); + +CREATE TABLE keto_relation_tuples_with_fk +( + shard_id TEXT NOT NULL, + nid TEXT NOT NULL, + namespace_id INTEGER NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INTEGER NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + + PRIMARY KEY (shard_id, nid), + + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + + -- enforce to have exactly one of subject_id or subject_set + CONSTRAINT chk_keto_rt_subject_type CHECK + ((subject_id IS NULL AND + subject_set_namespace_id IS NOT NULL AND subject_set_object IS NOT NULL AND subject_set_relation IS NOT NULL) + OR + (subject_id IS NOT NULL AND + subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)) +); + +INSERT INTO keto_relation_tuples_with_fk (shard_id, nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, subject_set_object, subject_set_relation, commit_time) SELECT * FROM keto_relation_tuples; + +DROP TABLE keto_relation_tuples; + +ALTER TABLE keto_relation_tuples_with_fk RENAME TO keto_relation_tuples; + +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id) WHERE subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL; + +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_set_namespace_id, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; + +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, + namespace_id, + object, + relation, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + commit_time); diff --git a/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.up.sql b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.up.sql new file mode 100644 index 0000000..828c637 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220217152313000000_nid_fk.up.sql @@ -0,0 +1,7 @@ +-- These tuples can't be retrieved over any API (and also not inserted by any API) because we always add a `WHERE nid = ?` clause to the queries. +-- This DELETE statement is here to ensure that we don't run into problems where the `nid` column is not properly set. Should not happen if only the official API is used. +DELETE FROM keto_relation_tuples WHERE nid NOT IN (SELECT nid FROM networks); + +ALTER TABLE keto_relation_tuples + ADD CONSTRAINT keto_relation_tuples_nid_fk + FOREIGN KEY (nid) REFERENCES networks (id); diff --git a/internal/persistence/sql/migrations/sql/20220512151000000000_indices.down.sql b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.down.sql new file mode 100644 index 0000000..15e41a9 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.down.sql @@ -0,0 +1,2 @@ +DROP INDEX keto_relation_tuples_reverse_subject_ids_idx; +DROP INDEX keto_relation_tuples_reverse_subject_sets_idx; diff --git a/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.down.sql b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.down.sql new file mode 100644 index 0000000..b88a29d --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX keto_relation_tuples_reverse_subject_idx ON keto_relation_tuples; diff --git a/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.up.sql b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.up.sql new file mode 100644 index 0000000..8c935ce --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.mysql.up.sql @@ -0,0 +1,8 @@ +CREATE INDEX keto_relation_tuples_reverse_subject_idx ON keto_relation_tuples (nid, + subject_id, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + relation, + namespace_id + ); diff --git a/internal/persistence/sql/migrations/sql/20220512151000000000_indices.up.sql b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.up.sql new file mode 100644 index 0000000..98f5724 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220512151000000000_indices.up.sql @@ -0,0 +1,13 @@ +CREATE INDEX keto_relation_tuples_reverse_subject_ids_idx ON keto_relation_tuples (nid, + subject_id, + relation, + namespace_id + ) WHERE subject_set_namespace_id IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL; + +CREATE INDEX keto_relation_tuples_reverse_subject_sets_idx ON keto_relation_tuples (nid, + subject_set_namespace_id, + subject_set_object, + subject_set_relation, + relation, + namespace_id + ) WHERE subject_id IS NULL; diff --git a/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.down.sql b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.down.sql new file mode 100644 index 0000000..c55c118 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples_uuid; diff --git a/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.mysql.up.sql b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.mysql.up.sql new file mode 100644 index 0000000..aeda5cf --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.mysql.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE keto_relation_tuples_uuid +( + shard_id CHAR(36) NOT NULL, + nid CHAR(36) NOT NULL, + namespace VARCHAR(200) NOT NULL, + object CHAR(36) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id CHAR(36) NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object CHAR(36) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id ASC, nid ASC), + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + + -- mysql has no partial indexes so we can only use the full one + INDEX keto_relation_tuples_uuid_full_idx (nid, namespace, object, relation, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, commit_time), + INDEX keto_relation_tuples_uuid_reverse_subject_idx (nid, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, relation, namespace), + + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); diff --git a/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.postgres.up.sql b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.postgres.up.sql new file mode 100644 index 0000000..b05b557 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.postgres.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE keto_relation_tuples_uuid +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace VARCHAR(200) NOT NULL, + object UUID NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id UUID NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object UUID NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); +CREATE INDEX keto_relation_tuples_uuid_subject_ids_idx ON keto_relation_tuples_uuid (nid, namespace, object, relation, subject_id) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_uuid_subject_sets_idx ON keto_relation_tuples_uuid (nid, namespace, object, relation, subject_set_namespace, subject_set_object, subject_set_relation) WHERE subject_id IS NULL; +CREATE INDEX keto_relation_tuples_uuid_full_idx ON keto_relation_tuples_uuid (nid, namespace, object, relation, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, commit_time); +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx ON keto_relation_tuples_uuid (nid, subject_id, relation, namespace) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx ON keto_relation_tuples_uuid (nid, subject_set_namespace, subject_set_object, subject_set_relation, relation, namespace) WHERE subject_id IS NULL; diff --git a/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.sqlite.up.sql b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.sqlite.up.sql new file mode 100644 index 0000000..6f6816d --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.sqlite.up.sql @@ -0,0 +1,41 @@ +CREATE TABLE keto_relation_tuples_uuid +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace VARCHAR(200) NOT NULL, + object UUID NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id UUID NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object UUID NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); + +CREATE INDEX keto_relation_tuples_uuid_subject_ids_idx ON keto_relation_tuples_uuid (nid, namespace, object, relation, subject_id) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_uuid_subject_sets_idx ON keto_relation_tuples_uuid (nid, namespace, object, + relation, + subject_set_namespace, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; +CREATE INDEX keto_relation_tuples_uuid_full_idx ON keto_relation_tuples_uuid (nid, namespace, object, + relation, subject_id, + subject_set_namespace, + subject_set_object, + subject_set_relation, commit_time + ); +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx ON keto_relation_tuples_uuid (nid, subject_id, relation, namespace) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx ON keto_relation_tuples_uuid (nid, + subject_set_namespace, + subject_set_object, + subject_set_relation, + relation, + namespace + ) WHERE subject_id IS NULL; diff --git a/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.up.sql b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.up.sql new file mode 100644 index 0000000..404979e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200300000000_create-intermediary-uuid-table.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE keto_relation_tuples_uuid +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace VARCHAR(200) NOT NULL, + object UUID NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id UUID NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object UUID NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + INDEX keto_relation_tuples_uuid_subject_ids_idx (nid, namespace, object, relation, subject_id) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL), + INDEX keto_relation_tuples_uuid_subject_sets_idx (nid, namespace, object, relation, subject_set_namespace, subject_set_object, subject_set_relation) WHERE subject_id IS NULL, + INDEX keto_relation_tuples_uuid_full_idx (nid, namespace, object, relation, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, commit_time), + INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx (nid, subject_id, relation, namespace) WHERE ((subject_set_namespace IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL), + INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx (nid, subject_set_namespace, subject_set_object, subject_set_relation, relation, namespace) WHERE subject_id IS NULL, + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.down.sql b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.down.sql new file mode 100644 index 0000000..09ae2c1 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.down.sql @@ -0,0 +1 @@ +DROP TABLE keto_uuid_mappings; diff --git a/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.mysql.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.mysql.up.sql new file mode 100644 index 0000000..55cf18a --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.mysql.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE keto_uuid_mappings +( + id CHAR(36) NOT NULL, + string_representation TEXT NOT NULL CHECK (string_representation <> ''), + + PRIMARY KEY (id) +); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.up.sql new file mode 100644 index 0000000..28e758f --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000000_create-uuid-mapping-table.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE keto_uuid_mappings +( + id UUID NOT NULL PRIMARY KEY, + string_representation TEXT NOT NULL CHECK (string_representation <> '') +); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.down.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.down.sql new file mode 100644 index 0000000..e2511ea --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings ADD CONSTRAINT check_string_representation CHECK (string_representation <> ''); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.up.sql new file mode 100644 index 0000000..8bdb0f4 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings DROP CONSTRAINT IF EXISTS check_string_representation; diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.down.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.down.sql new file mode 100644 index 0000000..71f4ed7 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings ADD CONSTRAINT keto_uuid_mappings_chk_1 CHECK (string_representation <> ''); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.up.sql new file mode 100644 index 0000000..5b784f9 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings DROP CONSTRAINT keto_uuid_mappings_chk_1; diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.down.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.down.sql new file mode 100644 index 0000000..27c6133 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings ADD CONSTRAINT keto_uuid_mappings_string_representation_check CHECK (string_representation <> ''); diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.up.sql new file mode 100644 index 0000000..eecf229 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE keto_uuid_mappings DROP CONSTRAINT IF EXISTS keto_uuid_mappings_string_representation_check; diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.down.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.down.sql new file mode 100644 index 0000000..ee0bf31 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.down.sql @@ -0,0 +1 @@ +-- do nothing. \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.up.sql b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.up.sql new file mode 100644 index 0000000..0e3fd3f --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200400000001_uuid-mapping-remove-check.sqlite.up.sql @@ -0,0 +1,9 @@ +--ALTER TABLE keto_uuid_mappings DROP CONSTRAINT IF EXISTS check_string_representation; +ALTER TABLE keto_uuid_mappings RENAME TO keto_uuid_mapping_with_old_check_constraint; +CREATE TABLE keto_uuid_mappings +( + id UUID NOT NULL PRIMARY KEY, + string_representation TEXT NOT NULL +); +INSERT INTO keto_uuid_mappings SELECT * FROM keto_uuid_mapping_with_old_check_constraint; +DROP TABLE keto_uuid_mapping_with_old_check_constraint; diff --git a/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.down.sql b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.down.sql new file mode 100644 index 0000000..f067827 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.down.sql @@ -0,0 +1,25 @@ +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INT8 NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INT8 NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + INDEX keto_relation_tuples_subject_ids_idx (nid, namespace_id, object, relation, subject_id) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL), + INDEX keto_relation_tuples_subject_sets_idx (nid, namespace_id, object, relation, subject_set_namespace_id, subject_set_object, subject_set_relation) WHERE subject_id IS NULL, + INDEX keto_relation_tuples_full_idx (nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, subject_set_object, subject_set_relation, commit_time), + INDEX keto_relation_tuples_reverse_subject_ids_idx (nid, subject_id, relation, namespace_id) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL), + INDEX keto_relation_tuples_reverse_subject_sets_idx (nid, subject_set_namespace_id, subject_set_object, subject_set_relation, relation, namespace_id) WHERE subject_id IS NULL, + CONSTRAINT chk_keto_rt_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace_id IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace_id IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); diff --git a/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.mysql.down.sql b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.mysql.down.sql new file mode 100644 index 0000000..9290298 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.mysql.down.sql @@ -0,0 +1,25 @@ +CREATE TABLE keto_relation_tuples +( + shard_id CHAR(36) NOT NULL, + nid CHAR(36) NOT NULL, + namespace_id INT8 NOT NULL, + object CHAR(36) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id CHAR(36) NULL, + subject_set_namespace_id INT8 NULL, + subject_set_object CHAR(36) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id ASC, nid ASC), + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + + -- mysql has no partial indexes so we can only use the full one + INDEX keto_relation_tuples_full_idx (nid, namespace_id, object, relation, subject_id, subject_set_namespace_id, subject_set_object, subject_set_relation, commit_time), + INDEX keto_relation_tuples_reverse_subject_idx (nid, subject_id, subject_set_namespace_id, subject_set_object, subject_set_relation, relation, namespace_id), + + CONSTRAINT chk_keto_rt_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace_id IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace_id IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); diff --git a/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.postgres.down.sql b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.postgres.down.sql new file mode 100644 index 0000000..d2954ce --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.postgres.down.sql @@ -0,0 +1,34 @@ +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INT8 NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INT8 NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + CONSTRAINT chk_keto_rt_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace_id IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace_id IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid, namespace_id, object, relation, subject_id) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid, namespace_id, object, relation, + subject_set_namespace_id, + subject_set_object, + subject_set_relation) WHERE subject_id IS NULL; +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid, namespace_id, object, relation, subject_id, + subject_set_namespace_id, subject_set_object, + subject_set_relation, commit_time); +CREATE INDEX keto_relation_tuples_reverse_subject_ids_idx ON keto_relation_tuples (nid, subject_id, relation, namespace_id) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_reverse_subject_sets_idx ON keto_relation_tuples (nid, subject_set_namespace_id, + subject_set_object, + subject_set_relation, relation, + namespace_id) WHERE subject_id IS NULL; + diff --git a/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.sqlite.down.sql b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.sqlite.down.sql new file mode 100644 index 0000000..6cfae1e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.sqlite.down.sql @@ -0,0 +1,36 @@ +CREATE TABLE keto_relation_tuples +( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace_id INT8 NOT NULL, + object VARCHAR(64) NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id VARCHAR(64) NULL, + subject_set_namespace_id INT8 NULL, + subject_set_object VARCHAR(64) NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + CONSTRAINT "primary" PRIMARY KEY (shard_id ASC, nid ASC), + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + CONSTRAINT chk_keto_rt_subject_type CHECK (((((subject_id IS NULL) AND (subject_set_namespace_id IS NOT NULL)) AND + (subject_set_object IS NOT NULL)) AND + (subject_set_relation IS NOT NULL)) OR + ((((subject_id IS NOT NULL) AND (subject_set_namespace_id IS NULL)) AND + (subject_set_object IS NULL)) AND (subject_set_relation IS NULL))) +); + +CREATE INDEX keto_relation_tuples_subject_ids_idx ON keto_relation_tuples (nid ASC, namespace_id ASC, object ASC, relation ASC, subject_id ASC) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_subject_sets_idx ON keto_relation_tuples (nid ASC, namespace_id ASC, object ASC, + relation ASC, subject_set_namespace_id ASC, + subject_set_object ASC, subject_set_relation + ASC) WHERE subject_id IS NULL; +CREATE INDEX keto_relation_tuples_full_idx ON keto_relation_tuples (nid ASC, namespace_id ASC, object ASC, relation ASC, + subject_id ASC, subject_set_namespace_id ASC, + subject_set_object ASC, subject_set_relation ASC, + commit_time ASC); +CREATE INDEX keto_relation_tuples_reverse_subject_ids_idx ON keto_relation_tuples (nid ASC, subject_id ASC, relation ASC, namespace_id ASC) WHERE ((subject_set_namespace_id IS NULL) AND (subject_set_object IS NULL)) AND (subject_set_relation IS NULL); +CREATE INDEX keto_relation_tuples_reverse_subject_sets_idx ON keto_relation_tuples (nid ASC, subject_set_namespace_id + ASC, subject_set_object ASC, + subject_set_relation ASC, relation + ASC, namespace_id + ASC) WHERE subject_id IS NULL; diff --git a/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.up.sql b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.up.sql new file mode 100644 index 0000000..abe576e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000000_drop-old-non-uuid-table.up.sql @@ -0,0 +1 @@ +DROP TABLE keto_relation_tuples; diff --git a/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.down.sql b/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.down.sql new file mode 100644 index 0000000..a52186f --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.down.sql @@ -0,0 +1 @@ +ALTER TABLE keto_relation_tuples RENAME TO keto_relation_tuples_uuid; diff --git a/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.up.sql b/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.up.sql new file mode 100644 index 0000000..a63ce3f --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20220513200600000001_drop-old-non-uuid-table.up.sql @@ -0,0 +1 @@ +ALTER TABLE keto_relation_tuples_uuid RENAME TO keto_relation_tuples; diff --git a/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.down.sql b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.down.sql new file mode 100644 index 0000000..8d4964f --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE + keto_relation_tuples DROP CONSTRAINT keto_relation_tuples_nid_fk; + +ALTER TABLE + keto_relation_tuples +ADD + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id); \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.down.sql b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.down.sql new file mode 100644 index 0000000..2f6d777 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.down.sql @@ -0,0 +1,109 @@ +ALTER TABLE + keto_relation_tuples RENAME TO _keto_relation_tuples; + +DROP INDEX keto_relation_tuples_uuid_subject_ids_idx; + +DROP INDEX keto_relation_tuples_uuid_subject_sets_idx; + +DROP INDEX keto_relation_tuples_uuid_full_idx; + +DROP INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx; + +DROP INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx; + +CREATE TABLE keto_relation_tuples ( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace VARCHAR(200) NOT NULL, + object UUID NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id UUID NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object UUID NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_uuid_nid_fk FOREIGN KEY (nid) REFERENCES networks (id), + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK ( + ( + ( + ( + (subject_id IS NULL) + AND (subject_set_namespace IS NOT NULL) + ) + AND (subject_set_object IS NOT NULL) + ) + AND (subject_set_relation IS NOT NULL) + ) + OR ( + ( + ( + (subject_id IS NOT NULL) + AND (subject_set_namespace IS NULL) + ) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL) + ) + ) +); + +CREATE INDEX keto_relation_tuples_uuid_subject_ids_idx ON keto_relation_tuples (nid, namespace, object, relation, subject_id) +WHERE + ( + (subject_set_namespace IS NULL) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL); + +CREATE INDEX keto_relation_tuples_uuid_subject_sets_idx ON keto_relation_tuples ( + nid, + namespace, + object, + relation, + subject_set_namespace, + subject_set_object, + subject_set_relation +) +WHERE + subject_id IS NULL; + +CREATE INDEX keto_relation_tuples_uuid_full_idx ON keto_relation_tuples ( + nid, + namespace, + object, + relation, + subject_id, + subject_set_namespace, + subject_set_object, + subject_set_relation, + commit_time +); + +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx ON keto_relation_tuples (nid, subject_id, relation, namespace) +WHERE + ( + (subject_set_namespace IS NULL) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL); + +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx ON keto_relation_tuples ( + nid, + subject_set_namespace, + subject_set_object, + subject_set_relation, + relation, + namespace +) +WHERE + subject_id IS NULL; + +INSERT INTO + keto_relation_tuples +SELECT + * +FROM + _keto_relation_tuples; + +DROP TABLE _keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.up.sql b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.up.sql new file mode 100644 index 0000000..6ed2745 --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.sqlite.up.sql @@ -0,0 +1,109 @@ +ALTER TABLE + keto_relation_tuples RENAME TO _keto_relation_tuples; + +DROP INDEX keto_relation_tuples_uuid_subject_ids_idx; + +DROP INDEX keto_relation_tuples_uuid_subject_sets_idx; + +DROP INDEX keto_relation_tuples_uuid_full_idx; + +DROP INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx; + +DROP INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx; + +CREATE TABLE keto_relation_tuples ( + shard_id UUID NOT NULL, + nid UUID NOT NULL, + namespace VARCHAR(200) NOT NULL, + object UUID NOT NULL, + relation VARCHAR(64) NOT NULL, + subject_id UUID NULL, + subject_set_namespace VARCHAR(200) NULL, + subject_set_object UUID NULL, + subject_set_relation VARCHAR(64) NULL, + commit_time TIMESTAMP NOT NULL, + PRIMARY KEY (shard_id, nid), + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE, + CONSTRAINT chk_keto_rt_uuid_subject_type CHECK ( + ( + ( + ( + (subject_id IS NULL) + AND (subject_set_namespace IS NOT NULL) + ) + AND (subject_set_object IS NOT NULL) + ) + AND (subject_set_relation IS NOT NULL) + ) + OR ( + ( + ( + (subject_id IS NOT NULL) + AND (subject_set_namespace IS NULL) + ) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL) + ) + ) +); + +CREATE INDEX keto_relation_tuples_uuid_subject_ids_idx ON keto_relation_tuples (nid, namespace, object, relation, subject_id) +WHERE + ( + (subject_set_namespace IS NULL) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL); + +CREATE INDEX keto_relation_tuples_uuid_subject_sets_idx ON keto_relation_tuples ( + nid, + namespace, + object, + relation, + subject_set_namespace, + subject_set_object, + subject_set_relation +) +WHERE + subject_id IS NULL; + +CREATE INDEX keto_relation_tuples_uuid_full_idx ON keto_relation_tuples ( + nid, + namespace, + object, + relation, + subject_id, + subject_set_namespace, + subject_set_object, + subject_set_relation, + commit_time +); + +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_ids_idx ON keto_relation_tuples (nid, subject_id, relation, namespace) +WHERE + ( + (subject_set_namespace IS NULL) + AND (subject_set_object IS NULL) + ) + AND (subject_set_relation IS NULL); + +CREATE INDEX keto_relation_tuples_uuid_reverse_subject_sets_idx ON keto_relation_tuples ( + nid, + subject_set_namespace, + subject_set_object, + subject_set_relation, + relation, + namespace +) +WHERE + subject_id IS NULL; + +INSERT INTO + keto_relation_tuples +SELECT + * +FROM + _keto_relation_tuples; + +DROP TABLE _keto_relation_tuples; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.up.sql b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.up.sql new file mode 100644 index 0000000..8c5294e --- /dev/null +++ b/internal/persistence/sql/migrations/sql/20230228091200000000_add-on-delete-cascade-to-relationship.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE + keto_relation_tuples DROP CONSTRAINT keto_relation_tuples_uuid_nid_fk, +ADD + CONSTRAINT keto_relation_tuples_nid_fk FOREIGN KEY (nid) REFERENCES networks (id) ON UPDATE RESTRICT ON DELETE CASCADE; \ No newline at end of file diff --git a/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator.go b/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator.go new file mode 100644 index 0000000..80df7c4 --- /dev/null +++ b/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator.go @@ -0,0 +1,427 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package uuidmapping + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/jmoiron/sqlx" + "github.com/ory/pop/v6" + "github.com/ory/x/popx" + "github.com/ory/x/sqlcon" + "github.com/pkg/errors" + + "github.com/ory/keto/internal/namespace" +) + +//lint:file-ignore SA1019 as we migrate legacy stuff + +// We copy the definitions of OldRelationTuple and UUIDMapping here so that the +// migration will always work on the same definitions. +type ( + OldRelationTuple struct { + // An ID field is required to make pop happy. The actual ID is a + // composite primary key. + ID uuid.UUID `db:"shard_id"` + NetworkID uuid.UUID `db:"nid"` + NamespaceID int32 `db:"namespace_id"` + Object string `db:"object"` + Relation string `db:"relation"` + SubjectID sql.NullString `db:"subject_id"` + SubjectSetNamespaceID sql.NullInt32 `db:"subject_set_namespace_id"` + SubjectSetObject sql.NullString `db:"subject_set_object"` + SubjectSetRelation sql.NullString `db:"subject_set_relation"` + CommitTime time.Time `db:"commit_time"` + } + NewRelationTuple struct { + ID uuid.UUID `db:"shard_id"` + NetworkID uuid.UUID `db:"nid"` + Namespace string `db:"namespace"` + Object uuid.UUID `db:"object"` + Relation string `db:"relation"` + SubjectID uuid.NullUUID `db:"subject_id"` + SubjectSetNamespace sql.NullString `db:"subject_set_namespace"` + SubjectSetObject uuid.NullUUID `db:"subject_set_object"` + SubjectSetRelation sql.NullString `db:"subject_set_relation"` + CommitTime time.Time `db:"commit_time"` + } + UUIDMapping struct { + ID uuid.UUID `db:"id"` + StringRepresentation string `db:"string_representation"` + } + UUIDMappings []*UUIDMapping + ColumnProvider interface { + dbFields() []any + } + ColumnNameAble interface { + columns() []string + } +) + +const ( + RelationTupleTableName = "keto_relation_tuples" + RelationTupleUUIDTableName = "keto_relation_tuples_uuid" + UUIDMappingTableName = "keto_uuid_mappings" + MigrationVersion = "20220513200500000000" +) + +func (OldRelationTuple) TableName() string { return RelationTupleTableName } +func (NewRelationTuple) TableName() string { return RelationTupleUUIDTableName } + +func (rt *OldRelationTuple) dbFields() []any { + return []any{ + rt.ID, rt.NetworkID, + rt.NamespaceID, rt.Object, rt.Relation, + rt.SubjectID, + rt.SubjectSetNamespaceID, rt.SubjectSetObject, rt.SubjectSetRelation, + rt.CommitTime, + } +} +func (OldRelationTuple) columns() []string { + return []string{ + "shard_id", "nid", + "namespace_id", "object", "relation", + "subject_id", + "subject_set_namespace_id", "subject_set_object", "subject_set_relation", + "commit_time", + } +} + +func (rt *NewRelationTuple) dbFields() []any { + return []any{ + rt.ID, rt.NetworkID, + rt.Namespace, rt.Object, rt.Relation, + rt.SubjectID, + rt.SubjectSetNamespace, rt.SubjectSetObject, rt.SubjectSetRelation, + rt.CommitTime, + } +} +func (NewRelationTuple) columns() []string { + return []string{ + "shard_id", "nid", + "namespace", "object", "relation", + "subject_id", + "subject_set_namespace", "subject_set_object", "subject_set_relation", + "commit_time", + } +} + +func (UUIDMappings) TableName() string { return UUIDMappingTableName } +func (UUIDMapping) TableName() string { return UUIDMappingTableName } + +func (m *UUIDMapping) dbFields() []any { return []any{m.ID, m.StringRepresentation} } +func (UUIDMapping) columns() []string { return []string{"id", "string_representation"} } + +func (rt *OldRelationTuple) ToUUID(s string) uuid.UUID { + return uuid.NewV5(rt.NetworkID, s) +} + +func namespaceIDtoName(n namespace.Manager, id int32) (string, error) { + //nolint:staticcheck + ns, err := n.GetNamespaceByConfigID(context.Background(), id) + if err != nil { + return "", err + } + return ns.Name, nil +} + +func (rt *OldRelationTuple) ToNew(n namespace.Manager) (newRT *NewRelationTuple, objectMapping *UUIDMapping, subjectMapping *UUIDMapping, err error) { + newRT = &NewRelationTuple{ + ID: rt.ID, + NetworkID: rt.NetworkID, + Object: uuid.NewV5(rt.NetworkID, rt.Object), + Relation: rt.Relation, + SubjectID: uuid.NullUUID{ + Valid: rt.SubjectID.Valid, + UUID: uuid.NewV5(rt.NetworkID, rt.SubjectID.String), + }, + SubjectSetObject: uuid.NullUUID{ + Valid: rt.SubjectSetObject.Valid, + UUID: uuid.NewV5(rt.NetworkID, rt.SubjectSetObject.String), + }, + SubjectSetRelation: rt.SubjectSetRelation, + CommitTime: rt.CommitTime, + } + + if newRT.Namespace, err = namespaceIDtoName(n, rt.NamespaceID); err != nil { + return + } + if rt.SubjectSetNamespaceID.Valid { + newRT.SubjectSetNamespace.Valid = true + if newRT.SubjectSetNamespace.String, err = namespaceIDtoName(n, rt.SubjectSetNamespaceID.Int32); err != nil { + return + } + } + objectMapping = &UUIDMapping{ + ID: newRT.Object, + StringRepresentation: rt.Object, + } + + switch { + case rt.SubjectID.Valid: + subjectMapping = &UUIDMapping{ + ID: newRT.SubjectID.UUID, + StringRepresentation: rt.SubjectID.String, + } + case rt.SubjectSetObject.Valid: + subjectMapping = &UUIDMapping{ + ID: newRT.SubjectSetObject.UUID, + StringRepresentation: rt.SubjectSetObject.String, + } + } + return +} + +var ( + name = "migrate-strings-to-uuids" + Migrations = func(namespaces namespace.Manager) popx.Migrations { + return popx.Migrations{ + // The "up" migration will add the UUID mappings to the database and + // replace the strings with UUIDs. + { + Version: MigrationVersion, + Name: name, + Path: name, + Direction: "up", + DBType: "all", + Type: "go", + Runner: func(_ popx.Migration, conn *pop.Connection) error { + var ( + relationTuples []OldRelationTuple + err error + lastID uuid.UUID + ) + for { + relationTuples, err = GetRelationTuples[OldRelationTuple](conn, lastID) + if err != nil { + return fmt.Errorf("could not get old relationships: %w", err) + } + if len(relationTuples) == 0 { + break + } + lastID = relationTuples[len(relationTuples)-1].ID + + mappings := make([]*UUIDMapping, len(relationTuples)*2) + newTuples := make([]*NewRelationTuple, len(relationTuples)) + for i := range relationTuples { + newTuples[i], mappings[i*2], mappings[i*2+1], err = relationTuples[i].ToNew(namespaces) + if err != nil { + return errors.WithStack(err) + } + } + + if err := BatchWriteMappings(conn, mappings); err != nil { + return fmt.Errorf("could not write mappings: %w", err) + } + if err := BatchInsertTuples(conn, newTuples); err != nil { + return fmt.Errorf("could not insert new tuples: %w", err) + } + } + + return nil + }, + }, + // The "down" migration will replace all UUIDs with strings from the + // mapping table. + { + Version: MigrationVersion, + Name: name, + Path: name, + Direction: "down", + DBType: "all", + Type: "go", + Runner: func(_ popx.Migration, conn *pop.Connection) error { + var ( + relationTuples []NewRelationTuple + err error + lastID uuid.UUID + ) + for { + relationTuples, err = GetRelationTuples[NewRelationTuple](conn, lastID) + if err != nil { + return fmt.Errorf("could not get new relationships: %w", err) + } + if len(relationTuples) == 0 { + break + } + lastID = relationTuples[len(relationTuples)-1].ID + + mappings := make(map[uuid.UUID][]*string, len(relationTuples)*2) + oldTuples := make([]*OldRelationTuple, len(relationTuples)) + ctx := context.Background() + for i, rt := range relationTuples { + ot := &OldRelationTuple{ + ID: rt.ID, + NetworkID: rt.NetworkID, + Relation: rt.Relation, + SubjectID: sql.NullString{ + Valid: rt.SubjectID.Valid, // Value will be set below + }, + SubjectSetObject: sql.NullString{ + Valid: rt.SubjectSetObject.Valid, // Value will be set below + }, + SubjectSetRelation: rt.SubjectSetRelation, + CommitTime: rt.CommitTime, + } + + namespace, err := namespaces.GetNamespaceByName(ctx, rt.Namespace) + if err != nil { + return fmt.Errorf("could not get namespace: %w", err) + } + ot.NamespaceID = namespace.ID //nolint:staticcheck + + if rt.SubjectSetNamespace.Valid { + subjectSetNamespace, err := namespaces.GetNamespaceByName(ctx, rt.SubjectSetNamespace.String) + if err != nil { + return fmt.Errorf("could not get subject namespace: %w", err) + } + //nolint:staticcheck + if err = ot.SubjectSetNamespaceID.Scan(subjectSetNamespace.ID); err != nil { + return err + } + } + + mappings[rt.Object] = append(mappings[rt.Object], &ot.Object) + switch { + case rt.SubjectID.Valid: + mappings[rt.SubjectID.UUID] = append(mappings[rt.SubjectID.UUID], &ot.SubjectID.String) + case rt.SubjectSetObject.Valid: + mappings[rt.SubjectSetObject.UUID] = append(mappings[rt.SubjectSetObject.UUID], &ot.SubjectSetObject.String) + } + oldTuples[i] = ot + } + if err := BatchReplaceUUIDs(conn, mappings); err != nil { + return fmt.Errorf("could not replace UUIDs: %w", err) + } + + if err := BatchInsertTuples(conn, oldTuples); err != nil { + return fmt.Errorf("could not insert old tuples: %w", err) + } + } + + return nil + }, + }, + } + } +) + +func ConstructArgs[T ColumnProvider](nCols int, items []T) (string, []interface{}) { + placeholderRow := "(" + strings.Repeat("?,", nCols-1) + "?)" + + q := &strings.Builder{} + q.Grow(len(items) * (len(placeholderRow) + 1)) + + args := make([]interface{}, 0, len(items)*nCols) + + q.WriteString(placeholderRow) + args = append(args, items[0].dbFields()...) + + for _, item := range items[1:] { + q.WriteRune(',') + q.WriteString(placeholderRow) + args = append(args, item.dbFields()...) + } + + return q.String(), args +} + +func GetRelationTuples[RT interface { + ColumnNameAble + pop.TableNameAble +}](conn *pop.Connection, lastID uuid.UUID) ( + res []RT, err error, +) { + const pageSize = 500 + var rt RT + + // Bind-placeholder syntax is a property of the driver, not the dialect + // name: sqlx maps drivers (e.g. "pgx") to $-placeholders. Use DefaultDriver + // so dialects that share a driver but report a distinct name — YugabyteDB, + // which uses "pgx" like postgres — get the correct placeholders. + q := sqlx.Rebind(sqlx.BindType(conn.Dialect.DefaultDriver()), "SELECT * FROM %s WHERE shard_id > ? ORDER BY shard_id LIMIT ?") + + err = conn.Store.Select( + &res, fmt.Sprintf(q, rt.TableName()), lastID, pageSize) + if err != nil { + return nil, sqlcon.HandleError(err) + } + return res, nil +} + +func BatchWriteMappings(conn *pop.Connection, mappings []*UUIDMapping) (err error) { + if len(mappings) == 0 { + // Nothing to do. + return nil + } + + placeholders, args := ConstructArgs(2, mappings) + + // We need to write manual SQL here because the INSERT should not fail if + // the UUID already exists, but we still want to return an error if anything + // else goes wrong. + var query string + switch d := conn.Dialect.Name(); d { + case "mysql": + query = `INSERT IGNORE INTO keto_uuid_mappings (id, string_representation) VALUES ` + placeholders + default: + query = ` + INSERT INTO keto_uuid_mappings (id, string_representation) + VALUES ` + placeholders + ` + ON CONFLICT (id) DO NOTHING` + } + + if err = sqlcon.HandleError(conn.RawQuery(query, args...).Exec()); err != nil { + return err + } + + return nil +} + +func BatchReplaceUUIDs(conn *pop.Connection, uuidToTargets map[uuid.UUID][]*string) error { + if len(uuidToTargets) == 0 { + return nil + } + + ids := make([]uuid.UUID, 0, len(uuidToTargets)) + for id := range uuidToTargets { + ids = append(ids, id) + } + + mappings := make([]UUIDMapping, 0, len(ids)) + query := conn.Where("id in (?)", ids) + if err := sqlcon.HandleError(query.All(&mappings)); err != nil { + return err + } + + // Write the representation to the correct pointer(s). + for _, m := range mappings { + for _, target := range uuidToTargets[m.ID] { + *target = m.StringRepresentation + } + } + + return nil +} + +func BatchInsertTuples[RT interface { + pop.TableNameAble + ColumnProvider + ColumnNameAble +}](conn *pop.Connection, rts []RT) error { + if len(rts) == 0 { + return nil + } + + cols := rts[0].columns() + placeholders, args := ConstructArgs(len(cols), rts) + query := fmt.Sprintf("INSERT INTO %s (%s) VALUES %s", rts[0].TableName(), strings.Join(cols, ", "), placeholders) + + return sqlcon.HandleError(conn.RawQuery(query, args...).Exec()) +} diff --git a/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator_test.go b/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator_test.go new file mode 100644 index 0000000..d9319e1 --- /dev/null +++ b/internal/persistence/sql/migrations/uuidmapping/uuid_mapping_migrator_test.go @@ -0,0 +1,49 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package uuidmapping_test + +import ( + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + + "github.com/ory/keto/internal/persistence/sql/migrations/uuidmapping" +) + +func TestConstructArgs(t *testing.T) { + createItems := func(n int) []uuidmapping.ColumnProvider { + items := make([]uuidmapping.ColumnProvider, n) + for i := range items { + items[i] = &uuidmapping.UUIDMapping{ + ID: uuid.Must(uuid.NewV4()), + StringRepresentation: "foo", + } + } + return items + } + for _, tc := range []struct { + desc string + items []uuidmapping.ColumnProvider + nCols int + }{ + { + desc: "only one item", + items: createItems(1), + nCols: 2, + }, + { + desc: "multiple items", + items: createItems(10), + nCols: 2, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + placeholders, args := uuidmapping.ConstructArgs(tc.nCols, tc.items) + assert.Len(t, args, tc.nCols*len(tc.items)) + assert.Len(t, args, strings.Count(placeholders, "?")) + }) + } +} diff --git a/internal/persistence/sql/persister.go b/internal/persistence/sql/persister.go new file mode 100644 index 0000000..86bb7cc --- /dev/null +++ b/internal/persistence/sql/persister.go @@ -0,0 +1,149 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "embed" + + "github.com/gofrs/uuid" + "github.com/ory/pop/v6" + "github.com/ory/x/contextx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" + + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/ory/x/pagination/paginationplanner" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/persistence" +) + +type ( + Persister struct { + conn *pop.Connection + d dependencies + nid uuid.UUID + + planner *paginationplanner.PaginationPlanner + } + dependencies interface { + logrusx.Provider + otelx.Provider + contextx.Provider + config.Provider + + PopConnection(ctx context.Context) (*pop.Connection, error) + MapperNamespace(ctx context.Context) uuid.UUID + NewShardID() uuid.UUID + } +) + +var ( + //go:embed migrations/sql/*.sql + Migrations embed.FS + + _ persistence.Persister = &Persister{} +) + +// Pagination Planner for relation_tuple table +var ( + t = paginationplanner.NewTable() + TupleColNamespace = t.NewColumn("namespace") + TupleColObject = t.NewColumn("object") + TupleColRel = t.NewColumn("relation") + TupleColSubjectId = t.NewColumn("subject_id") + TupleColSubjectSetNamespace = t.NewColumn("subject_set_namespace") + TupleColSubjectSetObject = t.NewColumn("subject_set_object") + TupleColSubjectSetRel = t.NewColumn("subject_set_relation") +) + +var ( + fullIdxPlan = paginationplanner.PaginationPlan{ + DefaultPageToken: keysetpagination.NewPageToken([]keysetpagination.Column{ + {Name: TupleColNamespace.Name(), Order: keysetpagination.OrderAscending, Value: ""}, + {Name: TupleColObject.Name(), Order: keysetpagination.OrderAscending, Value: uuid.Nil}, + {Name: TupleColRel.Name(), Order: keysetpagination.OrderAscending, Value: ""}, + {Name: TupleColSubjectId.Name(), Order: keysetpagination.OrderAscending, Nullable: true}, + {Name: TupleColSubjectSetNamespace.Name(), Order: keysetpagination.OrderAscending, Nullable: true}, + {Name: TupleColSubjectSetObject.Name(), Order: keysetpagination.OrderAscending, Nullable: true}, + {Name: TupleColSubjectSetRel.Name(), Order: keysetpagination.OrderAscending, Nullable: true}, + {Name: "commit_time", Order: keysetpagination.OrderAscending, Value: "1970-01-01"}, + {Name: "shard_id", Order: keysetpagination.OrderAscending, Value: uuid.Nil}, + }...), + ApplicableQueries: [][]paginationplanner.Column{ + {}, + {TupleColNamespace}, + {TupleColNamespace, TupleColObject}, + {TupleColNamespace, TupleColObject, TupleColRel}, + }, + } + + fallbackPlan = paginationplanner.PaginationPlan{ + DefaultPageToken: keysetpagination.NewPageToken([]keysetpagination.Column{ + {Name: "shard_id", Order: keysetpagination.OrderAscending, Value: uuid.Nil}, + }...), + } +) + +func NewPersister(ctx context.Context, reg dependencies, nid uuid.UUID) (*Persister, error) { + conn, err := reg.PopConnection(ctx) + if err != nil { + return nil, err + } + + planner, err := paginationplanner.NewPaginationPlanner(fallbackPlan, []paginationplanner.PaginationPlan{fullIdxPlan}) + if err != nil { + return nil, err + } + p := &Persister{ + d: reg, + nid: nid, + conn: conn, + planner: planner, + } + return p, nil +} + +func (p *Persister) Connection(ctx context.Context) *pop.Connection { + return popx.GetConnection(ctx, p.conn.WithContext(ctx)) +} + +func (p *Persister) queryWithNetwork(ctx context.Context) *pop.Query { + return p.Connection(ctx).Where("nid = ?", p.NetworkID(ctx)) +} + +func (p *Persister) Transaction(ctx context.Context, f func(ctx context.Context) error) error { + return popx.Transaction(ctx, p.conn.WithContext(ctx), func(ctx context.Context, _ *pop.Connection) error { return f(ctx) }) +} + +func (p *Persister) WithLatestSnapshot(ctx context.Context, f func(ctx context.Context) error) error { + conn := p.conn.WithContext(ctx) + + // Reuse an existing transaction rather than nesting. + if popx.GetConnection(ctx, conn).TX != nil { + return f(ctx) + } + + // REPEATABLE READ pins a consistent snapshot so all queries in the check see + // the same database state. ReadOnly enforces read-only at the DB level. + // CockroachDB promotes that level to SERIALIZABLE; SQLite ignores + // the isolation level entirely. + return popx.TransactionWithOptions(ctx, conn, &sql.TxOptions{ + Isolation: sql.LevelRepeatableRead, + ReadOnly: true, + }, func(ctx context.Context, _ *pop.Connection) error { + return f(ctx) + }) +} + +func (p *Persister) NetworkID(ctx context.Context) uuid.UUID { + return p.d.Contextualizer().Network(ctx, p.nid) +} + +func (p *Persister) SetNetwork(nid uuid.UUID) { + p.nid = nid +} diff --git a/internal/persistence/sql/query_test.go b/internal/persistence/sql/query_test.go new file mode 100644 index 0000000..aa9d5bf --- /dev/null +++ b/internal/persistence/sql/query_test.go @@ -0,0 +1,128 @@ +package sql + +import ( + "database/sql" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/ory/x/uuidx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" +) + +func TestBuildDelete(t *testing.T) { + t.Parallel() + nid := uuidx.NewV4() + + q, args, err := buildDelete(nid, nil) + assert.Error(t, err) + assert.Empty(t, q) + assert.Empty(t, args) + + obj1, obj2, sub1, obj3 := uuidx.NewV4(), uuidx.NewV4(), uuidx.NewV4(), uuidx.NewV4() + + q, args, err = buildDelete(nid, []*relationtuple.RelationTuple{ + { + Namespace: "ns1", + Object: obj1, + Relation: "rel1", + Subject: &relationtuple.SubjectID{ + ID: sub1, + }, + }, + { + Namespace: "ns2", + Object: obj2, + Relation: "rel2", + Subject: &relationtuple.SubjectSet{ + Namespace: "ns3", + Object: obj3, + Relation: "rel3", + }, + }, + }) + require.NoError(t, err) + + // parentheses are important here + assert.Equal(t, q, "DELETE FROM keto_relation_tuples WHERE ((namespace = ? AND object = ? AND relation = ? AND subject_id = ? AND subject_set_namespace IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL) OR (namespace = ? AND object = ? AND relation = ? AND subject_id IS NULL AND subject_set_namespace = ? AND subject_set_object = ? AND subject_set_relation = ?)) AND nid = ?") + assert.Equal(t, []any{"ns1", obj1, "rel1", sub1, "ns2", obj2, "rel2", "ns3", obj3, "rel3", nid}, args) +} + +func TestBuildInsert(t *testing.T) { + t.Parallel() + nid := uuidx.NewV4() + + q, args, err := buildInsert(uuidx.NewV4, time.Now(), nid, nil) + assert.Error(t, err) + assert.Empty(t, q) + assert.Empty(t, args) + + obj1, obj2, sub1, obj3 := uuidx.NewV4(), uuidx.NewV4(), uuidx.NewV4(), uuidx.NewV4() + + now := time.Now() + + q, args, err = buildInsert(uuidx.NewV4, now, nid, []*relationtuple.RelationTuple{ + { + Namespace: "ns1", + Object: obj1, + Relation: "rel1", + Subject: &relationtuple.SubjectID{ + ID: sub1, + }, + }, + { + Namespace: "ns2", + Object: obj2, + Relation: "rel2", + Subject: &relationtuple.SubjectSet{ + Namespace: "ns3", + Object: obj3, + Relation: "rel3", + }, + }, + }) + require.NoError(t, err) + + assert.Equal(t, q, "INSERT INTO keto_relation_tuples (shard_id, nid, namespace, object, relation, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, commit_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") + assert.Equal(t, []any{ + args[0], // this is kind of cheating but we generate the shard id in the buildInsert function + nid, + "ns1", + obj1, + "rel1", + uuid.NullUUID{UUID: sub1, Valid: true}, + sql.NullString{}, uuid.NullUUID{}, sql.NullString{}, + now.UTC(), + + args[10], // again, cheating + nid, + "ns2", + obj2, + "rel2", + uuid.NullUUID{}, + sql.NullString{String: "ns3", Valid: true}, uuid.NullUUID{UUID: obj3, Valid: true}, sql.NullString{String: "rel3", Valid: true}, + now.UTC(), + }, args) +} + +func TestBuildInsertUUIDs(t *testing.T) { + t.Parallel() + + foo, bar, baz := uuidx.NewV4(), uuidx.NewV4(), uuidx.NewV4() + uuids := []UUIDMapping{ + {foo, "foo"}, + {bar, "bar"}, + {baz, "baz"}, + } + + q, args := buildInsertUUIDs(uuids, "mysql") + assert.Equal(t, "INSERT IGNORE INTO keto_uuid_mappings (id, string_representation) VALUES (?,?),(?,?),(?,?)", q) + assert.Equal(t, []any{foo, "foo", bar, "bar", baz, "baz"}, args) + + q, args = buildInsertUUIDs(uuids, "anything else") + assert.Equal(t, "INSERT INTO keto_uuid_mappings (id, string_representation) VALUES (?,?),(?,?),(?,?) ON CONFLICT (id) DO NOTHING", q) + assert.Equal(t, []any{foo, "foo", bar, "bar", baz, "baz"}, args) +} diff --git a/internal/persistence/sql/relationtuples.go b/internal/persistence/sql/relationtuples.go new file mode 100644 index 0000000..d35ef61 --- /dev/null +++ b/internal/persistence/sql/relationtuples.go @@ -0,0 +1,368 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "database/sql" + "fmt" + "slices" + "strings" + "time" + + "github.com/gofrs/uuid" + "github.com/ory/pop/v6" + "github.com/ory/x/otelx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + paginationplanner "github.com/ory/x/pagination/paginationplanner" + + "github.com/ory/x/sqlcon" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +// Typical database limits for placeholders/bind vars are 1<<15 (32k, MySQL, SQLite) and 1<<16 (64k, PostgreSQL, CockroachDB). +const ( + chunkSizeInsertUUIDMappings = 15000 // two placeholders per mapping + chunkSizeInsertTuple = 3000 // ten placeholders per tuple + chunkSizeDeleteTuple = 100 // the database must build an expression tree for each chunk, so we must limit more aggressively +) + +type ( + RelationTuple struct { + // An ID field is required to make pop happy. The actual ID is a composite primary key. + ID uuid.UUID `db:"shard_id"` + NetworkID uuid.UUID `db:"nid"` + Namespace string `db:"namespace"` + Object uuid.UUID `db:"object"` + Relation string `db:"relation"` + SubjectID uuid.NullUUID `db:"subject_id"` + SubjectSetNamespace sql.NullString `db:"subject_set_namespace"` + SubjectSetObject uuid.NullUUID `db:"subject_set_object"` + SubjectSetRelation sql.NullString `db:"subject_set_relation"` + CommitTime time.Time `db:"commit_time"` + } + relationTuples []RelationTuple +) + +func (relationTuples) TableName() string { + return "keto_relation_tuples" +} + +func (*RelationTuple) TableName() string { + return "keto_relation_tuples" +} + +func (r *RelationTuple) ToInternal() *relationtuple.RelationTuple { + if r == nil { + return nil + } + + rt := &relationtuple.RelationTuple{ + Relation: r.Relation, + Object: r.Object, + Namespace: r.Namespace, + } + + if r.SubjectID.Valid { + rt.Subject = &relationtuple.SubjectID{ + ID: r.SubjectID.UUID, + } + } else { + rt.Subject = &relationtuple.SubjectSet{ + Namespace: r.SubjectSetNamespace.String, + Object: r.SubjectSetObject.UUID, + Relation: r.SubjectSetRelation.String, + } + } + + return rt +} + +func (r *RelationTuple) insertSubject(s relationtuple.Subject) error { + switch st := s.(type) { + case *relationtuple.SubjectID: + r.SubjectID = uuid.NullUUID{ + UUID: st.ID, + Valid: true, + } + r.SubjectSetNamespace = sql.NullString{} + r.SubjectSetObject = uuid.NullUUID{} + r.SubjectSetRelation = sql.NullString{} + case *relationtuple.SubjectSet: + r.SubjectID = uuid.NullUUID{} + _ = r.SubjectSetNamespace.Scan(st.Namespace) + _ = r.SubjectSetObject.Scan(st.Object) + _ = r.SubjectSetRelation.Scan(st.Relation) + } + return nil +} + +func (r *RelationTuple) FromInternal(rt *relationtuple.RelationTuple) (err error) { + r.Namespace = rt.Namespace + r.Object = rt.Object + r.Relation = rt.Relation + + return r.insertSubject(rt.Subject) +} + +func (p *Persister) whereSubject(_ context.Context, q *pop.Query, sub relationtuple.Subject) error { + switch s := sub.(type) { + case *relationtuple.SubjectID: + q. + Where("subject_id = ?", s.ID). + // NULL checks to leverage partial indexes + Where("subject_set_namespace IS NULL"). + Where("subject_set_object IS NULL"). + Where("subject_set_relation IS NULL") + case *relationtuple.SubjectSet: + q. + Where("subject_set_namespace = ?", s.Namespace). + Where("subject_set_object = ?", s.Object). + Where("subject_set_relation = ?", s.Relation). + // NULL checks to leverage partial indexes + Where("subject_id IS NULL") + case nil: + return errors.WithStack(ketoapi.ErrNilSubject()) + } + return nil +} + +func (p *Persister) whereQuery(ctx context.Context, q *pop.Query, rq *relationtuple.RelationQuery) error { + if rq.Namespace != nil { + q.Where("namespace = ?", *rq.Namespace) + } + if rq.Object != nil { + q.Where("object = ?", *rq.Object) + } + if rq.Relation != nil { + q.Where("relation = ?", *rq.Relation) + } + if s := rq.Subject; s != nil { + if err := p.whereSubject(ctx, q, s); err != nil { + return err + } + } + return nil +} + +// buildPlannerQuery sets the pagination planner constraints. +func buildPlannerQuery(rq *relationtuple.RelationQuery) paginationplanner.Query { + cs := paginationplanner.NewQuery() + if rq.Namespace != nil { + cs.SetEq(TupleColNamespace) + } + if rq.Object != nil { + cs.SetEq(TupleColObject) + } + if rq.Relation != nil { + cs.SetIsNull(TupleColRel) + } + if s := rq.Subject; s != nil { + switch s.(type) { + case *relationtuple.SubjectID: + cs.SetEq(TupleColSubjectId) + cs.SetIsNull(TupleColSubjectSetNamespace, TupleColSubjectSetObject, TupleColSubjectSetRel) + case *relationtuple.SubjectSet: + cs.SetIsNull(TupleColSubjectId) + cs.SetEq(TupleColSubjectSetNamespace, TupleColSubjectSetObject, TupleColSubjectSetRel) + } + } + return cs +} + +func buildDelete(nid uuid.UUID, rs []*relationtuple.RelationTuple) (query string, args []any, err error) { + if len(rs) == 0 { + return "", nil, errors.WithStack(ketoapi.ErrMalformedInput()) + } + + args = make([]any, 0, 6*len(rs)+1) + ors := make([]string, 0, len(rs)) + for _, rt := range rs { + switch s := rt.Subject.(type) { + case *relationtuple.SubjectID: + ors = append(ors, "(namespace = ? AND object = ? AND relation = ? AND subject_id = ? AND subject_set_namespace IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL)") + args = append(args, rt.Namespace, rt.Object, rt.Relation, s.ID) + case *relationtuple.SubjectSet: + ors = append(ors, "(namespace = ? AND object = ? AND relation = ? AND subject_id IS NULL AND subject_set_namespace = ? AND subject_set_object = ? AND subject_set_relation = ?)") + args = append(args, rt.Namespace, rt.Object, rt.Relation, s.Namespace, s.Object, s.Relation) + case nil: + return "", nil, errors.WithStack(ketoapi.ErrNilSubject()) + } + } + + query = fmt.Sprintf("DELETE FROM %s WHERE (%s) AND nid = ?", (&RelationTuple{}).TableName(), strings.Join(ors, " OR ")) + args = append(args, nid) + return query, args, nil +} + +func (p *Persister) DeleteRelationTuples(ctx context.Context, rs ...*relationtuple.RelationTuple) (err error) { + if len(rs) == 0 { + return nil + } + + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteRelationTuples", + trace.WithAttributes(attribute.Int("count", len(rs)))) + defer otelx.End(span, &err) + + return p.Transaction(ctx, func(ctx context.Context) error { + for chunk := range slices.Chunk(rs, chunkSizeDeleteTuple) { + q, args, err := buildDelete(p.NetworkID(ctx), chunk) + if err != nil { + return err + } + if q == "" { + continue + } + if err := p.Connection(ctx).RawQuery(q, args...).Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + return nil + }) +} + +func (p *Persister) DeleteAllRelationTuples(ctx context.Context, query *relationtuple.RelationQuery) (err error) { + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.DeleteAllRelationTuples") + defer otelx.End(span, &err) + + return p.Transaction(ctx, func(ctx context.Context) error { + sqlQuery := p.queryWithNetwork(ctx) + err := p.whereQuery(ctx, sqlQuery, query) + if err != nil { + return err + } + + var res relationTuples + return sqlQuery.Delete(&res) + }) +} + +func (p *Persister) GetRelationTuples(ctx context.Context, query *relationtuple.RelationQuery, pageOpts ...keysetpagination.Option) (_ []*relationtuple.RelationTuple, _ *keysetpagination.Paginator, err error) { + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.GetRelationTuples") + defer otelx.End(span, &err) + + constraints := buildPlannerQuery(query) + + sqlQuery := p.queryWithNetwork(ctx) + err = p.whereQuery(ctx, sqlQuery, query) + if err != nil { + return nil, nil, err + } + + paginator, err := p.planner.GetPaginator(constraints, pageOpts...) + if err != nil { + return nil, nil, err + } + sqlQuery = sqlQuery.Scope(keysetpagination.Paginate[*RelationTuple](paginator)) + + var res relationTuples + if err := sqlQuery.All(&res); err != nil { + return nil, nil, sqlcon.HandleError(err) + } + + res, nextPage := keysetpagination.Result(res, paginator) + internalRes := make([]*relationtuple.RelationTuple, len(res)) + for i := range res { + internalRes[i] = res[i].ToInternal() + } + + return internalRes, nextPage, nil +} + +func (p *Persister) ExistsRelationTuples(ctx context.Context, query *relationtuple.RelationQuery) (_ bool, err error) { + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.ExistsRelationTuples") + defer otelx.End(span, &err) + + sqlQuery := p.queryWithNetwork(ctx) + + err = p.whereQuery(ctx, sqlQuery, query) + if err != nil { + return false, err + } + exists, err := sqlQuery.Exists(&RelationTuple{}) + return exists, sqlcon.HandleError(err) +} + +func buildInsert(newShardID func() uuid.UUID, commitTime time.Time, nid uuid.UUID, rs []*relationtuple.RelationTuple) (query string, args []any, err error) { + if len(rs) == 0 { + return "", nil, errors.WithStack(ketoapi.ErrMalformedInput()) + } + + var q strings.Builder + fmt.Fprintf(&q, "INSERT INTO %s (shard_id, nid, namespace, object, relation, subject_id, subject_set_namespace, subject_set_object, subject_set_relation, commit_time) VALUES ", (&RelationTuple{}).TableName()) + const placeholders = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + const separator = ", " + q.Grow(len(rs) * (len(placeholders) + len(separator))) + args = make([]any, 0, 10*len(rs)) + + for i, r := range rs { + if r.Subject == nil { + return "", nil, errors.WithStack(ketoapi.ErrNilSubject()) + } + + rt := &RelationTuple{ + ID: newShardID(), + NetworkID: nid, + CommitTime: commitTime.UTC(), + } + if err := rt.FromInternal(r); err != nil { + return "", nil, err + } + + if i > 0 { + q.WriteString(separator) + } + q.WriteString(placeholders) + args = append(args, rt.ID, rt.NetworkID, rt.Namespace, rt.Object, rt.Relation, rt.SubjectID, rt.SubjectSetNamespace, rt.SubjectSetObject, rt.SubjectSetRelation, rt.CommitTime) + } + + query = q.String() + return query, args, nil +} + +func (p *Persister) WriteRelationTuples(ctx context.Context, rs ...*relationtuple.RelationTuple) (err error) { + if len(rs) == 0 { + return nil + } + + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.WriteRelationTuples", + trace.WithAttributes(attribute.Int("count", len(rs)))) + defer otelx.End(span, &err) + + commitTime := time.Now() + + return p.Transaction(ctx, func(ctx context.Context) error { + for chunk := range slices.Chunk(rs, chunkSizeInsertTuple) { + q, args, err := buildInsert(p.d.NewShardID, commitTime, p.NetworkID(ctx), chunk) + if err != nil { + return err + } + if err := p.Connection(ctx).RawQuery(q, args...).Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + return nil + }) +} + +func (p *Persister) TransactRelationTuples(ctx context.Context, ins []*relationtuple.RelationTuple, del []*relationtuple.RelationTuple) (err error) { + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.TransactRelationTuples") + defer otelx.End(span, &err) + + if len(ins)+len(del) == 0 { + return nil + } + + return p.Transaction(ctx, func(ctx context.Context) error { + if err := p.WriteRelationTuples(ctx, ins...); err != nil { + return err + } + return p.DeleteRelationTuples(ctx, del...) + }) +} diff --git a/internal/persistence/sql/relationtuples_test.go b/internal/persistence/sql/relationtuples_test.go new file mode 100644 index 0000000..7c8dba8 --- /dev/null +++ b/internal/persistence/sql/relationtuples_test.go @@ -0,0 +1,109 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + stdSql "database/sql" + "strings" + "testing" + "time" + + "github.com/ory/x/networkx" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/persistence/sql" + "github.com/ory/keto/internal/x/dbx" +) + +func rt(nw *networkx.Network, setSID, setNID, setO, setR bool) *sql.RelationTuple { + return &sql.RelationTuple{ + ID: uuid.Must(uuid.NewV4()), + NetworkID: nw.ID, + SubjectID: uuid.NullUUID{Valid: setSID}, + SubjectSetNamespace: stdSql.NullString{Valid: setNID}, + SubjectSetObject: uuid.NullUUID{Valid: setO}, + SubjectSetRelation: stdSql.NullString{Valid: setR}, + CommitTime: time.Now(), + } +} + +func TestRelationTupleSubjectTypeCheck(t *testing.T) { + t.Parallel() + + for _, dsn := range dbx.GetDSNs(t) { + t.Run("dsn="+dsn.Name, func(t *testing.T) { + t.Parallel() + reg := driver.NewTestRegistry(t, dsn) + c, err := reg.PopConnection(t.Context()) + require.NoError(t, err) + nw, err := reg.DetermineNetwork(t.Context()) + require.NoError(t, err) + + for _, tc := range []struct { + desc string + setSID, setNID, setO, setR, success bool + }{ + { + desc: "all", + setSID: true, + setNID: true, + setO: true, + setR: true, + success: false, + }, + { + desc: "nothing", + setSID: false, + setNID: false, + setO: false, + setR: false, + success: false, + }, + { + desc: "subject set", + setSID: false, + setNID: true, + setO: true, + setR: true, + success: true, + }, + { + desc: "subject ID", + setSID: true, + setNID: false, + setO: false, + setR: false, + success: true, + }, + { + desc: "incomplete subject set", + setSID: false, + setNID: true, + setO: true, + setR: false, + success: false, + }, + } { + t.Run("case="+tc.desc, func(t *testing.T) { + t.Parallel() + err := c.Create(rt(nw, tc.setSID, tc.setNID, tc.setO, tc.setR)) + + if tc.success { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.True(t, + strings.Contains(err.Error(), "chk_keto_rt_subject_type") || // <- normal databases + strings.Contains(err.Error(), "chk_keto_rt_uuid_subject_type") || // <- normal databases + strings.Contains(err.Error(), "SQLSTATE 23514")) // <- mysql + } + }) + } + }) + } +} diff --git a/internal/persistence/sql/traverser.go b/internal/persistence/sql/traverser.go new file mode 100644 index 0000000..1f8e51b --- /dev/null +++ b/internal/persistence/sql/traverser.go @@ -0,0 +1,218 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "context" + "fmt" + "strings" + + "github.com/gofrs/uuid" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +type ( + subjectExpandedRelationTupleRow struct { + RelationTuple + + Found bool `db:"found"` + } +) + +func whereSubject(sub relationtuple.Subject) (sqlFragment string, args []any, err error) { + switch s := sub.(type) { + case *relationtuple.SubjectID: + sqlFragment = "subject_id = ? AND subject_set_namespace IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL" + args = []any{s.ID} + + case *relationtuple.SubjectSet: + sqlFragment = "subject_id IS NULL AND subject_set_namespace = ? AND subject_set_object = ? AND subject_set_relation = ?" + args = []any{s.Namespace, s.Object, s.Relation} + + case nil: + return "", nil, errors.WithStack(ketoapi.ErrNilSubject()) + } + return sqlFragment, args, nil +} + +// buildSubjectSetTypeSQL builds an optional SQL fragment and args to filter +// subject-set tuples by (namespace, relation) pairs. Returns empty string and +// nil args when allowedSubjectSets is nil (no filter applied). +func buildSubjectSetTypeSQL(allowedSubjectSets []relationtuple.SubjectSetType) (string, []any) { + if len(allowedSubjectSets) == 0 { + return "", nil + } + args := make([]any, 0, len(allowedSubjectSets)*2) + for _, f := range allowedSubjectSets { + args = append(args, f.Namespace, f.Relation) + } + return "\nAND (current.subject_set_namespace, current.subject_set_relation) IN (" + tuplePlaceholders(len(allowedSubjectSets)) + ")", args +} + +// tuplePlaceholders returns n comma-separated "(?, ?)" pairs for use in a +// row-value IN list. +func tuplePlaceholders(n int) string { + return strings.TrimSuffix(strings.Repeat("(?, ?), ", n), ", ") +} + +// buildFoundExpr builds the SQL expression for the found column. +// +// When allowedSubjectSets is empty, found is the result of the EXISTS subquery for every row. +// When non-empty, AllowsDirect on each entry controls whether found can be true for that type: +// - no entries have AllowsDirect: found is always false (literal, no subquery). +// - all entries have AllowsDirect: found is the EXISTS result for every row. +// - mixed: found can be true only for rows whose type is in the AllowsDirect set +func buildFoundExpr(allowedSubjectSets []relationtuple.SubjectSetType, subject relationtuple.Subject) (string, []any, error) { + var directTypes []relationtuple.SubjectSetType + if len(allowedSubjectSets) > 0 { + for _, t := range allowedSubjectSets { + if t.AllowsDirect { + directTypes = append(directTypes, t) + } + } + if len(directTypes) == 0 { + return "false", nil, nil + } + } + + subjectSQL, subjectArgs, err := whereSubject(subject) + if err != nil { + return "", nil, err + } + + existsQuery := fmt.Sprintf(`EXISTS( + SELECT 1 FROM keto_relation_tuples + WHERE nid = current.nid AND + namespace = current.subject_set_namespace AND + object = current.subject_set_object AND + relation = current.subject_set_relation AND + %s + )`, subjectSQL) + + // No filter, or all types allow direct membership: EXISTS runs for every matched row. + if len(allowedSubjectSets) == 0 || len(directTypes) == len(allowedSubjectSets) { + return existsQuery, subjectArgs, nil + } + + // Only some types allow direct membership: gate the EXISTS result to those + // types. The EXISTS subquery may still be evaluated for every returned row. + args := make([]any, 0, len(subjectArgs)+len(directTypes)*2) + args = append(args, subjectArgs...) + for _, t := range directTypes { + args = append(args, t.Namespace, t.Relation) + } + return fmt.Sprintf("(%s AND (current.subject_set_namespace, current.subject_set_relation) IN (%s))", existsQuery, tuplePlaceholders(len(directTypes))), args, nil +} + +// TraverseSubjectSetExpansion gets all subject-set pointers for the object#relation and +// checks whether the requested subject is a direct member of each. When allowedSubjectSets +// is non-empty, only pointers matching the declared (namespace, relation) pairs are returned, +// and AllowsDirect on each type controls whether a direct membership check(subquery) is performed. +func (t *Persister) TraverseSubjectSetExpansion(ctx context.Context, start *relationtuple.RelationTuple, allowedSubjectSets []relationtuple.SubjectSetType) (res []*relationtuple.TraversalResult, err error) { + ctx, span := t.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.TraverseSubjectSetExpansion") + defer otelx.End(span, &err) + + var rowCount int + defer func() { span.SetAttributes(attribute.Int("tuples_loaded", rowCount)) }() + + foundExpr, foundArgs, err := buildFoundExpr(allowedSubjectSets, start.Subject) + if err != nil { + return nil, err + } + + subjectSetFilterSQL, subjectSetFilterArgs := buildSubjectSetTypeSQL(allowedSubjectSets) + shardID := uuid.Nil + for { + var ( + rows []*subjectExpandedRelationTupleRow + limit = 1000 + ) + queryArgs := make([]any, 0) + queryArgs = append(queryArgs, foundArgs...) + queryArgs = append(queryArgs, t.NetworkID(ctx), shardID, start.Namespace, start.Object, start.Relation) + queryArgs = append(queryArgs, subjectSetFilterArgs...) + queryArgs = append(queryArgs, limit) + + err = t.Connection(ctx).RawQuery(fmt.Sprintf(` +SELECT current.shard_id AS shard_id, + current.subject_set_namespace AS namespace, + current.subject_set_object AS object, + current.subject_set_relation AS relation, + %s AS found +FROM keto_relation_tuples AS current +WHERE current.nid = ? AND + current.shard_id > ? AND + current.namespace = ? AND + current.object = ? AND + current.relation = ? AND + current.subject_id IS NULL + %s +ORDER BY current.shard_id +LIMIT ? +`, foundExpr, subjectSetFilterSQL), + queryArgs..., + ).All(&rows) + if err != nil { + return nil, sqlcon.HandleError(err) + } + rowCount += len(rows) + + for _, r := range rows { + to := r.ToInternal() + to.Subject = start.Subject + res = append(res, &relationtuple.TraversalResult{ + From: start, + To: to, + Via: relationtuple.TraversalSubjectSetExpand, + Found: r.Found, + }) + if r.Found { + return res, nil + } + } + if len(rows) == limit { + shardID = rows[limit-1].ID + } else { + break + } + } + + return res, nil +} + +func (t *Persister) FindTupleWithRelations(ctx context.Context, tuple *relationtuple.RelationTuple, relations []string) (_ *relationtuple.RelationTuple, err error) { + ctx, span := t.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindTupleWithRelations") + defer otelx.End(span, &err) + + if len(relations) == 0 { + return nil, nil + } + var rows relationTuples + + query := t.queryWithNetwork(ctx) + if err := t.whereQuery(ctx, query, &relationtuple.RelationQuery{ + Namespace: &tuple.Namespace, + Object: &tuple.Object, + Subject: tuple.Subject, + }); err != nil { + return nil, err + } + err = query.Where("relation IN (?)", relations).Limit(1).All(&rows) + if err != nil { + return nil, sqlcon.HandleError(err) + } + + // If we got any rows back, success! + if len(rows) > 0 { + return rows[0].ToInternal(), nil + } + + return nil, nil +} diff --git a/internal/persistence/sql/traverser_test.go b/internal/persistence/sql/traverser_test.go new file mode 100644 index 0000000..c4179ce --- /dev/null +++ b/internal/persistence/sql/traverser_test.go @@ -0,0 +1,142 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" +) + +// existsFor returns the EXISTS subquery text that buildFoundExpr produces for the given subject. +func existsFor(t *testing.T, subject relationtuple.Subject) string { + t.Helper() + subjectSQL, _, err := whereSubject(subject) + require.NoError(t, err) + return `EXISTS( + SELECT 1 FROM keto_relation_tuples + WHERE nid = current.nid AND + namespace = current.subject_set_namespace AND + object = current.subject_set_object AND + relation = current.subject_set_relation AND + ` + subjectSQL + ` + )` +} + +func TestBuildFoundExpr(t *testing.T) { + t.Parallel() + t.Run("case=subject as SubjectID", func(t *testing.T) { + subject := &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())} + existsExpr := existsFor(t, subject) + + // when SubjectID is used, no `allowedSubjectSets` is expected + t.Run("no allowed types: EXISTS runs for all rows", func(t *testing.T) { + expr, args, err := buildFoundExpr(nil, subject) + require.NoError(t, err) + + assert.Equal(t, existsExpr, expr) + assert.Equal(t, []any{subject.ID}, args) + }) + }) + + t.Run("case=subject as SubjectSet", func(t *testing.T) { + subject := &relationtuple.SubjectSet{ + Namespace: "User", + Object: uuid.Must(uuid.NewV4()), + } + existsExpr := existsFor(t, subject) + + t.Run("no allowed types: EXISTS runs for all rows", func(t *testing.T) { + expr, args, err := buildFoundExpr(nil, subject) + require.NoError(t, err) + + assert.Equal(t, existsExpr, expr) + assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation}, args) + }) + + // All types have AllowsDirect=false: OPL forbids direct membership for every type, + // so the found column is always false without touching the inner table. + t.Run("all AllowsDirect=false: found is always false", func(t *testing.T) { + types := []relationtuple.SubjectSetType{ + {Namespace: "Group", Relation: "member", AllowsDirect: false}, + {Namespace: "Org", Relation: "admin", AllowsDirect: false}, + } + expr, args, err := buildFoundExpr(types, subject) + require.NoError(t, err) + + assert.Equal(t, "false", expr) + assert.Nil(t, args) + }) + + // All types allow direct: the WHERE clause already restricts rows to those types, + // so EXISTS runs unconditionally — no extra condition needed. + t.Run("all AllowsDirect=true: plain EXISTS, no extra conditions ", func(t *testing.T) { + types := []relationtuple.SubjectSetType{ + {Namespace: "Group", Relation: "member", AllowsDirect: true}, + {Namespace: "Org", Relation: "admin", AllowsDirect: true}, + } + expr, args, err := buildFoundExpr(types, subject) + require.NoError(t, err) + + assert.Equal(t, existsExpr, expr) + assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation}, args) + }) + + // Mixed: EXISTS is gated to fire only for the AllowsDirect=true type; + // the false type is excluded from the gate even though it passes the WHERE filter. + t.Run("mixed AllowsDirect: EXISTS gated only on allowed type", func(t *testing.T) { + types := []relationtuple.SubjectSetType{ + {Namespace: "Group", Relation: "member", AllowsDirect: true}, + {Namespace: "Org", Relation: "admin", AllowsDirect: false}, + } + expr, args, err := buildFoundExpr(types, subject) + require.NoError(t, err) + + assert.Equal(t, + `(`+existsExpr+` AND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?)))`, + expr, + ) + assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation, "Group", "member"}, args) + }) + + t.Run("mixed AllowsDirect: multiple allowed types with not-allowed type", func(t *testing.T) { + types := []relationtuple.SubjectSetType{ + {Namespace: "Group", Relation: "member", AllowsDirect: true}, + {Namespace: "Org", Relation: "admin", AllowsDirect: true}, + {Namespace: "Team", Relation: "lead", AllowsDirect: false}, + } + expr, args, err := buildFoundExpr(types, subject) + require.NoError(t, err) + + assert.Equal(t, + `(`+existsExpr+` AND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?), (?, ?)))`, + expr, + ) + assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation, "Group", "member", "Org", "admin"}, args) + }) + }) +} + +func TestBuildSubjectSetTypeSQL(t *testing.T) { + t.Parallel() + + t.Run("no types: no filter", func(t *testing.T) { + sqlFragment, args := buildSubjectSetTypeSQL(nil) + assert.Empty(t, sqlFragment) + assert.Nil(t, args) + }) + + t.Run("multiple types share one IN list", func(t *testing.T) { + sqlFragment, args := buildSubjectSetTypeSQL([]relationtuple.SubjectSetType{ + {Namespace: "Group", Relation: "member"}, + {Namespace: "Org", Relation: "admin"}, + }) + assert.Equal(t, "\nAND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?), (?, ?))", sqlFragment) + assert.Equal(t, []any{"Group", "member", "Org", "admin"}, args) + }) +} diff --git a/internal/persistence/sql/uuid_mapping.go b/internal/persistence/sql/uuid_mapping.go new file mode 100644 index 0000000..e792f9e --- /dev/null +++ b/internal/persistence/sql/uuid_mapping.go @@ -0,0 +1,186 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql + +import ( + "bytes" + "context" + "iter" + "maps" + "slices" + "strings" + + "github.com/gofrs/uuid" + "github.com/ory/x/otelx" + "github.com/ory/x/pagination/keysetpagination" + "github.com/ory/x/sqlcon" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type ( + UUIDMapping struct { + ID uuid.UUID `db:"id"` + StringRepresentation string `db:"string_representation"` + } + UUIDMappings []*UUIDMapping +) + +func (UUIDMappings) TableName() string { + return "keto_uuid_mappings" +} + +func (UUIDMapping) TableName() string { + return "keto_uuid_mappings" +} + +func (p *Persister) batchFromUUIDs(ctx context.Context, ids []uuid.UUID) (res []string, err error) { + if len(ids) == 0 { + return + } + + p.d.Logger().Trace("looking up UUIDs") + + // Build a map from UUID -> indices in the result. + idIdx := make(map[uuid.UUID][]int, len(ids)) + for i, id := range ids { + if ids, ok := idIdx[id]; ok { + idIdx[id] = append(ids, i) + } else { + idIdx[id] = []int{i} + } + } + nextID, stop := iter.Pull(maps.Keys(idIdx)) + defer stop() + + res = make([]string, len(ids)) + + idsToLookup := make([]uuid.UUID, 0, keysetpagination.DefaultMaxSize) + mappings := make([]UUIDMapping, 0, keysetpagination.DefaultMaxSize) + for i := 0; i < len(idIdx); i += keysetpagination.DefaultMaxSize { + idsToLookup = idsToLookup[:0] + mappings = mappings[:0] + + for range keysetpagination.DefaultMaxSize { + id, ok := nextID() + if !ok { + break + } + idsToLookup = append(idsToLookup, id) + } + query := p.Connection(ctx).Where("id in (?)", idsToLookup) + if err := sqlcon.HandleError(query.All(&mappings)); err != nil { + return []string{}, err + } + + // Write the representation to the correct index. + for _, m := range mappings { + for _, idx := range idIdx[m.ID] { + res[idx] = m.StringRepresentation + } + } + } + + return +} + +func (p *Persister) MapStringsToUUIDs(ctx context.Context, values ...string) (uuids []uuid.UUID, err error) { + if len(values) == 0 { + return + } + + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.MapStringsToUUIDs", + trace.WithAttributes(attribute.Int("num_values", len(values)))) + defer otelx.End(span, &err) + + uuids, err = p.MapStringsToUUIDsReadOnly(ctx, values...) + if err != nil { + return nil, err + } + + p.d.Logger().WithField("values", values).WithField("UUIDs", uuids).Trace("adding UUID mappings") + + mappings := make([]UUIDMapping, len(values)) + for i := range values { + mappings[i] = UUIDMapping{ + ID: uuids[i], + StringRepresentation: values[i], + } + } + slices.SortFunc(mappings, func(a, b UUIDMapping) int { + return bytes.Compare(a.ID[:], b.ID[:]) + }) + mappings = slices.CompactFunc(mappings, func(a, b UUIDMapping) bool { + return a.ID == b.ID + }) + + span.SetAttributes(attribute.Int("num_mappings", len(mappings))) + + err = p.Transaction(ctx, func(ctx context.Context) error { + for chunk := range slices.Chunk(mappings, chunkSizeInsertUUIDMappings) { + query, args := buildInsertUUIDs(chunk, p.conn.Dialect.Name()) + if err := p.Connection(ctx).RawQuery(query, args...).Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + return nil + }) + + return uuids, err +} + +func (p *Persister) MapStringsToUUIDsReadOnly(ctx context.Context, ss ...string) (uuids []uuid.UUID, err error) { + // This function doesn't talk to the database or do anything interesting, so we don't need to trace it. + // ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.MapStringsToUUIDsReadOnly") + // defer otelx.End(span, &err) + + uuids = make([]uuid.UUID, len(ss)) + for i := range ss { + uuids[i] = uuid.NewV5(p.d.MapperNamespace(ctx), ss[i]) + } + return uuids, nil +} + +func (p *Persister) MapUUIDsToStrings(ctx context.Context, u ...uuid.UUID) (_ []string, err error) { + ctx, span := p.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.MapUUIDsToStrings") + defer otelx.End(span, &err) + + return p.batchFromUUIDs(ctx, u) +} + +func buildInsertUUIDs(values []UUIDMapping, dialect string) (query string, args []any) { + if len(values) == 0 { + return "", nil + } + + const placeholder = "(?,?)" + const separator = "," + + var q strings.Builder + args = make([]any, 0, len(values)*2) + + if dialect == "mysql" { + q.WriteString("INSERT IGNORE INTO keto_uuid_mappings (id, string_representation) VALUES ") + } else { + q.WriteString("INSERT INTO keto_uuid_mappings (id, string_representation) VALUES ") + } + + q.Grow(len(values)*(len(placeholder)+len(separator)) + 100) + + for i, val := range values { + if i > 0 { + q.WriteString(separator) + } + q.WriteString(placeholder) + args = append(args, val.ID, val.StringRepresentation) + } + + if dialect == "mysql" { + // nothing + } else { + q.WriteString(" ON CONFLICT (id) DO NOTHING") + } + + return q.String(), args +} diff --git a/internal/persistence/sql/uuid_mapping_test.go b/internal/persistence/sql/uuid_mapping_test.go new file mode 100644 index 0000000..ebf90b7 --- /dev/null +++ b/internal/persistence/sql/uuid_mapping_test.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sql_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/persistence/sql" + "github.com/ory/keto/internal/x/dbx" +) + +func assertCheckErr(t assert.TestingT, err error, msgAndArgs ...interface{}) bool { + t.(*testing.T).Helper() + if err == nil { + return assert.Fail(t, "Did not receive an error", msgAndArgs...) + } + + if strings.Contains(err.Error(), "keto_uuid_mappings") || // <- normal databases + strings.Contains(err.Error(), "string_representation") || // <- sqlite + strings.Contains(err.Error(), "SQLSTATE 23505") || // <- cockroach + strings.Contains(err.Error(), "SQLSTATE 23514") { // <- mysql + return true + } + return assert.Fail(t, fmt.Sprintf("Did not receive check error, got:\n%+v", err), msgAndArgs...) +} + +func TestUUIDMapping(t *testing.T) { + t.Parallel() + + for _, dsn := range dbx.GetDSNs(t) { + t.Run("dsn="+dsn.Name, func(t *testing.T) { + t.Parallel() + reg := driver.NewTestRegistry(t, dsn) + c, err := reg.PopConnection(t.Context()) + require.NoError(t, err) + + testUUID := uuid.Must(uuid.NewV4()) + + for _, tc := range []struct { + desc string + mappings interface{} + assertErr assert.ErrorAssertionFunc + }{{ + desc: "empty should not fail on constraint", + mappings: &sql.UUIDMapping{}, + assertErr: assert.NoError, + }, { + desc: "empty strings should not fail on constraint", + mappings: &sql.UUIDMapping{ID: uuid.Nil}, + assertErr: assert.NoError, + }, { + desc: "single with string rep should succeed", + mappings: &sql.UUIDMapping{StringRepresentation: "foo"}, + assertErr: assert.NoError, + }, { + desc: "two with same uuid should fail on constraint", + mappings: &[]sql.UUIDMapping{ + {ID: testUUID, StringRepresentation: "foo"}, + {ID: testUUID, StringRepresentation: "bar"}, + }, + assertErr: assertCheckErr, + }, { + desc: "two with same rep should succeed", + mappings: &[]sql.UUIDMapping{ + {ID: uuid.Must(uuid.NewV4()), StringRepresentation: "bar"}, + {ID: uuid.Must(uuid.NewV4()), StringRepresentation: "bar"}, + }, + assertErr: assert.NoError, + }} { + tc := tc + t.Run("case="+tc.desc, func(t *testing.T) { + t.Parallel() + err := c.Create(tc.mappings) + tc.assertErr(t, err) + }) + } + }) + } +} diff --git a/internal/relationtuple/definitions.go b/internal/relationtuple/definitions.go new file mode 100644 index 0000000..f8c5b5f --- /dev/null +++ b/internal/relationtuple/definitions.go @@ -0,0 +1,274 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "context" + "sync" + + "github.com/gofrs/uuid" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/ketoapi" +) + +type ( + ManagerProvider interface { + RelationTupleManager() Manager + } + + // SubjectSetType identifies a subject-set pointer by its target namespace and + // relation, as declared in OPL. AllowsDirect indicates that OPL permits a + // direct subject match for this type. + SubjectSetType struct { + Namespace string + Relation string + AllowsDirect bool + } + + Manager interface { + GetRelationTuples(ctx context.Context, query *RelationQuery, options ...keysetpagination.Option) ([]*RelationTuple, *keysetpagination.Paginator, error) + ExistsRelationTuples(ctx context.Context, query *RelationQuery) (bool, error) + WriteRelationTuples(ctx context.Context, rs ...*RelationTuple) error + DeleteRelationTuples(ctx context.Context, rs ...*RelationTuple) error + DeleteAllRelationTuples(ctx context.Context, query *RelationQuery) error + TransactRelationTuples(ctx context.Context, insert []*RelationTuple, delete []*RelationTuple) error + // TraverseSubjectSetExpansion gets all subject sets for the tuple's + // object#relation and checks whether the tuple's subject is a member of + // each. When allowedSubjectSets is non-empty, only subject-set pointers + // matching the declared (namespace, relation) pairs are considered. + TraverseSubjectSetExpansion(ctx context.Context, tuple *RelationTuple, allowedSubjectSets []SubjectSetType) ([]*TraversalResult, error) + // FindTupleWithRelations returns the first tuple that matches the given + // tuple on namespace, object, and subject and has any of the given + // relations, or nil if there is none. + FindTupleWithRelations(ctx context.Context, tuple *RelationTuple, relations []string) (*RelationTuple, error) + + // Transaction runs f inside a database transaction. Nested calls reuse + // the outer transaction. + Transaction(ctx context.Context, f func(ctx context.Context) error) error + // WithLatestSnapshot runs f inside a snapshot-isolated read transaction, + // ensuring all SQL queries within f see a consistent database state. + WithLatestSnapshot(ctx context.Context, f func(ctx context.Context) error) error + } + SubjectID struct { + ID uuid.UUID `json:"id"` + } + RelationQuery struct { + Namespace *string `json:"namespace"` + Object *uuid.UUID `json:"object"` + Relation *string `json:"relation"` + Subject Subject `json:"subject_id,omitempty"` + } + TupleData interface { + GetSubject() *rts.Subject + GetObject() string + GetNamespace() string + GetRelation() string + } + Subject interface { + Equals(Subject) bool + UniqueID() uuid.UUID + String() string + } + RelationTuple struct { + Namespace string `json:"namespace"` + Object uuid.UUID `json:"object"` + Relation string `json:"relation"` + Subject Subject `json:"subject"` + } + InternalRelationTuples []*RelationTuple + SubjectSet struct { + Namespace string `json:"namespace"` + Object uuid.UUID `json:"object"` + Relation string `json:"relation"` + } + + // TODO(hperl): Also use a ketoapi.Tree here. + Tree struct { + Type ketoapi.TreeNodeType `json:"type"` + Subject Subject `json:"subject"` + Children []*Tree `json:"children,omitempty"` + Truncation *Truncation `json:"truncation,omitempty"` + } + + Truncation struct { + Reason TruncationReason `json:"reason,omitempty"` + Cursor *ExpandCursor `json:"cursor,omitempty"` + } + + TruncationReason string + ExpandCursorKind string + ExpandCursor struct { + Kind ExpandCursorKind `json:"kind"` + SubjectSet *SubjectSet `json:"subject_set,omitempty"` + NextPageToken keysetpagination.PageToken `json:"next_page_token,omitempty"` + + // TraverseRelation is the ComputedRelation in TupleToSubjectSet relation + TraverseRelation *string `json:"traverse_relation,omitempty"` + } +) + +const ( + ExpandCursorKindDirect ExpandCursorKind = "direct" + ExpandCursorKindTTU ExpandCursorKind = "ttu" +) + +type ( + TraversalResult struct { + From *RelationTuple + To *RelationTuple + Via Traversal + Found bool + } + + Traversal string +) + +const ( + TraversalUnknown Traversal = "unknown" + TraversalSubjectSetExpand Traversal = "subject set expand" + TraversalComputedUserset Traversal = "computed userset" + TraversalTupleToUserset Traversal = "tuple to userset" +) + +const ( + TruncationReasonDepthLimit TruncationReason = "depth_limit" + TruncationReasonTupleLimit TruncationReason = "tuple_limit" + TruncationReasonCycle TruncationReason = "cycle" +) + +var _, _ Subject = (*SubjectID)(nil), (*SubjectSet)(nil) + +func (s *SubjectID) Equals(other Subject) bool { + uv, ok := other.(*SubjectID) + if !ok { + return false + } + return uv.ID == s.ID +} + +func (s *SubjectID) UniqueID() uuid.UUID { return s.ID } +func (s *SubjectID) String() string { return s.ID.String() } + +func (s *SubjectSet) Equals(other Subject) bool { + uv, ok := other.(*SubjectSet) + if !ok { + return false + } + return uv.Relation == s.Relation && uv.Object == s.Object && uv.Namespace == s.Namespace +} + +func (s *SubjectSet) UniqueID() uuid.UUID { + return uuid.NewV5(s.Object, s.Namespace+"-"+s.Relation) +} + +func (s *SubjectSet) String() string { + if s == nil { + return "" + } + return s.Namespace + ":" + s.Object.String() + "#" + s.Relation +} + +func (t *RelationTuple) ToQuery() *RelationQuery { + return &RelationQuery{ + Namespace: &t.Namespace, + Object: &t.Object, + Relation: &t.Relation, + Subject: t.Subject, + } +} + +func (t *RelationTuple) String() string { + if t == nil { + return "" + } + + subject := "" + if t.Subject != nil { + subject = t.Subject.String() + } + return t.Namespace + ":" + t.Object.String() + "#" + t.Relation + "@" + subject +} + +func (t *RelationTuple) FromProto(proto *rts.RelationTuple) *RelationTuple { + // TODO(hperl) + return t +} + +func (t *RelationTuple) ToProto() *rts.RelationTuple { + // TODO(hperl) + return &rts.RelationTuple{} +} + +type ManagerWrapper struct { + Reg ManagerProvider + PageOpts []keysetpagination.Option + RequestedPages []keysetpagination.PageToken + // lock is necessary so that GetRelationTuples() is safe for concurrency. + requestedPagesLock sync.Mutex +} + +var ( + _ Manager = (*ManagerWrapper)(nil) + _ ManagerProvider = (*ManagerWrapper)(nil) +) + +func NewManagerWrapper(_ any, reg ManagerProvider, options ...keysetpagination.Option) *ManagerWrapper { + return &ManagerWrapper{ + Reg: reg, + PageOpts: options, + } +} + +func (t *ManagerWrapper) GetRelationTuples(ctx context.Context, query *RelationQuery, options ...keysetpagination.Option) ([]*RelationTuple, *keysetpagination.Paginator, error) { + p, err := keysetpagination.NewPaginator(options...) + if err != nil { + return nil, nil, err + } + t.requestedPagesLock.Lock() + defer t.requestedPagesLock.Unlock() + t.RequestedPages = append(t.RequestedPages, p.PageToken()) + return t.Reg.RelationTupleManager().GetRelationTuples(ctx, query, append(t.PageOpts, options...)...) +} + +func (t *ManagerWrapper) ExistsRelationTuples(ctx context.Context, query *RelationQuery) (bool, error) { + return t.Reg.RelationTupleManager().ExistsRelationTuples(ctx, query) +} + +func (t *ManagerWrapper) WriteRelationTuples(ctx context.Context, rs ...*RelationTuple) error { + return t.Reg.RelationTupleManager().WriteRelationTuples(ctx, rs...) +} + +func (t *ManagerWrapper) DeleteRelationTuples(ctx context.Context, rs ...*RelationTuple) error { + return t.Reg.RelationTupleManager().DeleteRelationTuples(ctx, rs...) +} + +func (t *ManagerWrapper) DeleteAllRelationTuples(ctx context.Context, query *RelationQuery) error { + return t.Reg.RelationTupleManager().DeleteAllRelationTuples(ctx, query) +} + +func (t *ManagerWrapper) TransactRelationTuples(ctx context.Context, insert []*RelationTuple, delete []*RelationTuple) error { + return t.Reg.RelationTupleManager().TransactRelationTuples(ctx, insert, delete) +} + +func (t *ManagerWrapper) TraverseSubjectSetExpansion(ctx context.Context, tuple *RelationTuple, allowedSubjectSets []SubjectSetType) ([]*TraversalResult, error) { + return t.Reg.RelationTupleManager().TraverseSubjectSetExpansion(ctx, tuple, allowedSubjectSets) +} + +func (t *ManagerWrapper) FindTupleWithRelations(ctx context.Context, tuple *RelationTuple, relations []string) (*RelationTuple, error) { + return t.Reg.RelationTupleManager().FindTupleWithRelations(ctx, tuple, relations) +} + +func (t *ManagerWrapper) Transaction(ctx context.Context, f func(ctx context.Context) error) error { + return t.Reg.RelationTupleManager().Transaction(ctx, f) +} + +func (t *ManagerWrapper) WithLatestSnapshot(ctx context.Context, f func(ctx context.Context) error) error { + return t.Reg.RelationTupleManager().WithLatestSnapshot(ctx, f) +} + +func (t *ManagerWrapper) RelationTupleManager() Manager { + return t +} diff --git a/internal/relationtuple/definitions_test.go b/internal/relationtuple/definitions_test.go new file mode 100644 index 0000000..831bbc2 --- /dev/null +++ b/internal/relationtuple/definitions_test.go @@ -0,0 +1,94 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "fmt" + "testing" + + "github.com/gofrs/uuid" + + "github.com/stretchr/testify/assert" +) + +func TestSubject(t *testing.T) { + t.Run("method=equals", func(t *testing.T) { + a, b := uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()) + + for i, tc := range []struct { + a, b Subject + equals bool + }{ + { + a: &SubjectID{ID: a}, + b: &SubjectID{ID: a}, + equals: true, + }, + { + a: &SubjectID{ID: a}, + b: &SubjectID{ID: b}, + equals: false, + }, + { + a: &SubjectSet{}, + b: &SubjectID{}, + equals: false, + }, + { + a: &SubjectSet{ + Namespace: "1", + Object: a, + Relation: "R", + }, + b: &SubjectSet{ + Namespace: "1", + Object: a, + Relation: "R", + }, + equals: true, + }, + { + a: &SubjectSet{ + Object: a, + Relation: "R", + }, + b: &SubjectSet{ + Namespace: "1", + Object: a, + Relation: "R", + }, + equals: false, + }, + { + a: &SubjectSet{ + Namespace: "1", + Relation: "R", + }, + b: &SubjectSet{ + Namespace: "1", + Object: a, + Relation: "R", + }, + equals: false, + }, + { + a: &SubjectSet{ + Namespace: "1", + Object: a, + }, + b: &SubjectSet{ + Namespace: "1", + Object: a, + Relation: "R", + }, + equals: false, + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + assert.Equal(t, tc.equals, tc.a.Equals(tc.b)) + assert.Equal(t, tc.equals, tc.b.Equals(tc.a)) + }) + } + }) +} diff --git a/internal/relationtuple/handler.go b/internal/relationtuple/handler.go new file mode 100644 index 0000000..d631153 --- /dev/null +++ b/internal/relationtuple/handler.go @@ -0,0 +1,98 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "connectrpc.com/connect" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/x" +) + +type ( + handlerDeps interface { + ManagerProvider + MapperProvider + config.Provider + logrusx.Provider + httpx.WriterProvider + otelx.Provider + x.NetworkIDProvider + x.HandlerOptionsProvider + } + ReadHandler struct { + relationtuplesconnect.UnimplementedReadServiceHandler + d handlerDeps + } + WriteHandler struct { + relationtuplesconnect.UnimplementedWriteServiceHandler + d handlerDeps + } +) + +const ( + ReadRouteBase = "/relation-tuples" + WriteRouteBase = "/admin/relation-tuples" +) + +func NewReadHandler(d handlerDeps) *ReadHandler { return &ReadHandler{d: d} } +func NewWriteHandler(d handlerDeps) *WriteHandler { return &WriteHandler{d: d} } + +func (h *ReadHandler) RegisterReadRoutes(r *httprouterx.RouterPublic) { + r.GET(ReadRouteBase, h.getRelations) + + listRelationTuplesHandler := connect.NewUnaryHandler( + relationtuplesconnect.ReadServiceListRelationTuplesProcedure, + h.ListRelationTuples, + connect.WithSchema(rts.File_ory_keto_relation_tuples_v1alpha2_read_service_proto. + Services().ByName("ReadService"). + Methods().ByName("ListRelationTuples")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.ReadServiceListRelationTuplesProcedure, listRelationTuplesHandler) +} + +func (h *WriteHandler) RegisterWriteRoutes(r *httprouterx.RouterAdmin) { + r.PUT(WriteRouteBase, h.createRelation) + r.DELETE(WriteRouteBase, h.deleteRelations) + r.PATCH(WriteRouteBase, h.patchRelationTuples) + + writeServiceMethods := rts.File_ory_keto_relation_tuples_v1alpha2_write_service_proto.Services().ByName("WriteService").Methods() + transactRelationTuplesHandler := connect.NewUnaryHandler( + relationtuplesconnect.WriteServiceTransactRelationTuplesProcedure, + h.TransactRelationTuples, + connect.WithSchema(writeServiceMethods.ByName("TransactRelationTuples")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.WriteServiceTransactRelationTuplesProcedure, transactRelationTuplesHandler) + deleteRelationTuplesHandler := connect.NewUnaryHandler( + relationtuplesconnect.WriteServiceDeleteRelationTuplesProcedure, + h.DeleteRelationTuples, + connect.WithSchema(writeServiceMethods.ByName("DeleteRelationTuples")), + connect.WithHandlerOptions(h.d.HandlerOptions()...), + ) + r.Handle(relationtuplesconnect.WriteServiceDeleteRelationTuplesProcedure, deleteRelationTuplesHandler) +} + +func (h *ReadHandler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{ + rts.File_ory_keto_relation_tuples_v1alpha2_read_service_proto, + rts.File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto, + } +} + +func (h *WriteHandler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{ + rts.File_ory_keto_relation_tuples_v1alpha2_write_service_proto, + rts.File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto, + } +} diff --git a/internal/relationtuple/read_server.go b/internal/relationtuple/read_server.go new file mode 100644 index 0000000..6405159 --- /dev/null +++ b/internal/relationtuple/read_server.go @@ -0,0 +1,175 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "context" + "net/http" + + "connectrpc.com/connect" + "github.com/ory/herodot" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/ketoapi" +) + +var _ relationtuplesconnect.ReadServiceHandler = (*ReadHandler)(nil) + +type ( + queryWrapper struct { + *rts.RelationQuery + } + deprecatedQueryWrapper struct { + *rts.ListRelationTuplesRequest_Query + } +) + +func (q *queryWrapper) GetObject() *string { return q.Object } +func (q *queryWrapper) GetNamespace() *string { return q.Namespace } +func (q *queryWrapper) GetRelation() *string { return q.Relation } + +func (q *deprecatedQueryWrapper) GetObject() *string { + if q.Object == "" { + return nil + } + return new(q.Object) +} + +func (q *deprecatedQueryWrapper) GetNamespace() *string { + if q.Namespace == "" { + return nil + } + return new(q.Namespace) +} + +func (q *deprecatedQueryWrapper) GetRelation() *string { + if q.Relation == "" { + return nil + } + return new(q.Relation) +} + +func (h *ReadHandler) ListRelationTuples(ctx context.Context, req *connect.Request[rts.ListRelationTuplesRequest]) (*connect.Response[rts.ListRelationTuplesResponse], error) { + var q ketoapi.RelationQuery + + switch { + case req.Msg.RelationQuery != nil: + q.FromDataProvider(&queryWrapper{req.Msg.RelationQuery}) + case req.Msg.Query != nil: //nolint:staticcheck //lint:ignore SA1019 backwards compatibility + q.FromDataProvider(&deprecatedQueryWrapper{req.Msg.Query}) //nolint:staticcheck //lint:ignore SA1019 backwards compatibility + default: + return nil, herodot.ErrBadRequest().WithError("you must provide a query") + } + + iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, &q) + if err != nil { + return nil, err + } + + paginationKeys := h.d.Config(ctx).PaginationEncryptionKeys() + pageOpts := make([]keysetpagination.Option, 0, 2) + if req.Msg.PageSize > 0 { + pageOpts = append(pageOpts, keysetpagination.WithSize(int(req.Msg.PageSize))) + } + if req.Msg.PageToken != "" { + token, err := keysetpagination.ParsePageToken(paginationKeys, req.Msg.PageToken) + if err != nil { + return nil, herodot.ErrBadRequest().WithError(err.Error()) + } + pageOpts = append(pageOpts, keysetpagination.WithToken(token)) + } + ir, nextPage, err := h.d.RelationTupleManager().GetRelationTuples(ctx, iq, pageOpts...) + if err != nil { + return nil, err + } + relations, err := h.d.ReadOnlyMapper().ToTuple(ctx, ir...) + if err != nil { + return nil, err + } + + resp := &rts.ListRelationTuplesResponse{ + RelationTuples: make([]*rts.RelationTuple, len(ir)), + } + if !nextPage.IsLast() { + resp.NextPageToken = nextPage.PageToken().Encrypt(paginationKeys) + } + for i, r := range relations { + resp.RelationTuples[i] = r.ToProto() + } + + return connect.NewResponse(resp), nil +} + +// swagger:route GET /relation-tuples relationship getRelationships +// +// # Query relationships +// +// Get all relationships that match the query. Only the namespace field is required. +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: relationships +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-medium +func (h *ReadHandler) getRelations(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + q := r.URL.Query() + query, err := (&ketoapi.RelationQuery{}).FromURLQuery(q) + if err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error())) + return + } + + l := h.d.Logger() + for k := range q { + l = l.WithField(k, q.Get(k)) + } + l.Debug("querying relationships") + + paginationKeys := h.d.Config(ctx).PaginationEncryptionKeys() + paginationOpts, err := keysetpagination.ParseQueryParams(paginationKeys, q) + if err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error())) + return + } + + iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, query) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + ir, nextPage, err := h.d.RelationTupleManager().GetRelationTuples(ctx, iq, paginationOpts...) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + relations, err := h.d.ReadOnlyMapper().ToTuple(ctx, ir...) + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + resp := &ketoapi.GetResponse{ + RelationTuples: relations, + } + if !nextPage.IsLast() { + resp.NextPageToken = nextPage.PageToken().Encrypt(paginationKeys) + } + + h.d.Writer().Write(w, r, resp) +} diff --git a/internal/relationtuple/read_server_test.go b/internal/relationtuple/read_server_test.go new file mode 100644 index 0000000..ed0eaa7 --- /dev/null +++ b/internal/relationtuple/read_server_test.go @@ -0,0 +1,345 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple_test + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "testing" + + "github.com/ory/x/httprouterx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "google.golang.org/grpc" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/ketoapi" +) + +func TestReadHandlers(t *testing.T) { + ctx := context.Background() + r := httprouterx.NewRouterPublic() + reg := driver.NewSqliteTestRegistry(t) + h := relationtuple.NewReadHandler(reg) + h.RegisterReadRoutes(r) + ts, clientTLS := driver.HTTP2TestServer(t, r) + + var newNamespace func(*testing.T) *namespace.Namespace + { + nspaces := 0 + newNamespace = func(t *testing.T) *namespace.Namespace { + n := &namespace.Namespace{Name: fmt.Sprintf("relation tuple read test %d", nspaces)} + nspaces++ + require.NoError(t, reg.Config(ctx).Set(config.KeyNamespaces, []*namespace.Namespace{n})) + return n + } + } + + t.Run("method=get", func(t *testing.T) { + t.Run("case=empty response is not nil", func(t *testing.T) { + nspace := newNamespace(t) + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "namespace": {nspace.Name}, + }.Encode()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.Equal(t, "[]", gjson.GetBytes(body, "relation_tuples").Raw) + + var respMsg ketoapi.GetResponse + require.NoError(t, json.Unmarshal(body, &respMsg)) + + assert.Equal(t, ketoapi.GetResponse{ + RelationTuples: []*ketoapi.RelationTuple{}, + NextPageToken: "", + }, respMsg) + }) + + t.Run("case=returns tuples", func(t *testing.T) { + nspace := newNamespace(t) + tuples := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "o1", + Relation: "r1", + SubjectID: new("s1"), + }, + { + Namespace: nspace.Name, + Object: "o2", + Relation: "r2", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "o1", + Relation: "r1", + }, + }, + } + + testhelpers.MapAndInsertTuples(t, reg, tuples...) + + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "namespace": {nspace.Name}, + }.Encode()) + require.NoError(t, err) + + var respMsg ketoapi.GetResponse + require.NoError(t, json.NewDecoder(resp.Body).Decode(&respMsg)) + + assert.ElementsMatch(t, tuples, respMsg.RelationTuples) + assert.Equal(t, "", respMsg.NextPageToken) + }) + + t.Run("case=return tuples without namespace", func(t *testing.T) { + nspace := newNamespace(t) + + tuples := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "obj", + Relation: "r1", + SubjectID: new("s1"), + }, + } + + testhelpers.MapAndInsertTuples(t, reg, tuples...) + + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "object": {"obj"}, + }.Encode()) + require.NoError(t, err) + assert.Equal(t, resp.StatusCode, http.StatusOK) + + var respMsg ketoapi.GetResponse + require.NoError(t, json.NewDecoder(resp.Body).Decode(&respMsg)) + assert.Equal(t, 1, len(respMsg.RelationTuples)) + assert.Containsf(t, tuples, respMsg.RelationTuples[0], "expected to find %q in %q", respMsg.RelationTuples[0].String(), tuples) + assert.Equal(t, "", respMsg.NextPageToken) + }) + + t.Run("case=returns bad request on malformed subject", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "subject": {"not#a valid subject"}, + }.Encode()) + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("case=paginates", func(t *testing.T) { + nspace := newNamespace(t) + + tuples := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "o1", + Relation: "r1", + SubjectID: new("s1"), + }, + { + Namespace: nspace.Name, + Object: "o2", + Relation: "r2", + SubjectID: new("s2"), + }, + } + testhelpers.MapAndInsertTuples(t, reg, tuples...) + + var firstResp ketoapi.GetResponse + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "namespace": {nspace.Name}, + "page_size": {"1"}, + }.Encode()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + require.NoError(t, json.NewDecoder(resp.Body).Decode(&firstResp)) + require.Len(t, firstResp.RelationTuples, 1) + assert.Contains(t, tuples, firstResp.RelationTuples[0]) + assert.NotEqual(t, "", firstResp.NextPageToken) + + // second page + resp, err = ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "namespace": {nspace.Name}, + "page_size": {"1"}, + "page_token": {firstResp.NextPageToken}, + }.Encode()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + secondResp := ketoapi.GetResponse{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&secondResp)) + require.Len(t, secondResp.RelationTuples, 1) + + assert.NotEqual(t, firstResp.RelationTuples, secondResp.RelationTuples) + assert.Contains(t, tuples, secondResp.RelationTuples[0]) + assert.Equal(t, "", secondResp.NextPageToken) + }) + + t.Run("case=returs bad request on invalid page size", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + relationtuple.ReadRouteBase + "?" + url.Values{ + "page_size": {"foo"}, + }.Encode()) + require.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + assert.Contains(t, string(body), "invalid syntax") + }) + }) + + t.Run("method=grpc", func(t *testing.T) { + type requestEnhancer = func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery) + withRelationQuery := func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery) { + req.RelationQuery = query.ToProto() + } + withDeprecatedQuery := func(req *rts.ListRelationTuplesRequest, query *ketoapi.RelationQuery) { + pq := query.ToProto() + //nolint:staticcheck + req.Query = &rts.ListRelationTuplesRequest_Query{ //lint:ignore SA1019 backwards compatibility + Subject: pq.Subject, + } + if pq.Namespace != nil { + //nolint:staticcheck + req.Query.Namespace = *pq.Namespace //lint:ignore SA1019 backwards compatibility + } + if pq.Object != nil { + //nolint:staticcheck + req.Query.Object = *pq.Object //lint:ignore SA1019 backwards compatibility + } + if pq.Relation != nil { + //nolint:staticcheck + req.Query.Relation = *pq.Relation //lint:ignore SA1019 backwards compatibility + } + } + apiTuplesFromProto := func(t *testing.T, pts ...*rts.RelationTuple) []*ketoapi.RelationTuple { + actual := make([]*ketoapi.RelationTuple, len(pts)) + for i, rt := range pts { + var err error + actual[i], err = (&ketoapi.RelationTuple{}).FromDataProvider(rt) + require.NoError(t, err) + } + return actual + } + + con, err := grpc.NewClient(ts.Listener.Addr().String(), grpc.WithTransportCredentials(clientTLS)) + require.NoError(t, err) + + t.Run("method=list", func(t *testing.T) { + client := rts.NewReadServiceClient(con) + + for key, enhancer := range map[string]requestEnhancer{"relation query": withRelationQuery, "deprecated query": withDeprecatedQuery} { + t.Run("enhancer="+key, func(t *testing.T) { + t.Run("case=returns empty list on no tuples", func(t *testing.T) { + nspace := newNamespace(t) + req := &rts.ListRelationTuplesRequest{} + enhancer(req, &ketoapi.RelationQuery{ + Namespace: &nspace.Name, + }) + resp, err := client.ListRelationTuples(ctx, req) + require.NoError(t, err) + assert.Len(t, resp.RelationTuples, 0) + }) + + t.Run("case=gets tuples", func(t *testing.T) { + nspace := newNamespace(t) + tuples := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "o1", + Relation: "rel", + SubjectID: new("s1"), + }, + { + Namespace: nspace.Name, + Object: "o2", + Relation: "rel", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "o1", + Relation: "r1", + }, + }, + } + testhelpers.MapAndInsertTuples(t, reg, tuples...) + + req := &rts.ListRelationTuplesRequest{} + enhancer(req, &ketoapi.RelationQuery{ + Namespace: &nspace.Name, + }) + + resp, err := client.ListRelationTuples(ctx, req) + require.NoError(t, err) + assert.Len(t, resp.RelationTuples, 2) + + assert.ElementsMatch(t, tuples, apiTuplesFromProto(t, resp.RelationTuples...)) + }) + + t.Run("case=paginates", func(t *testing.T) { + nspace := newNamespace(t) + tuples := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "o1", + Relation: "rel", + SubjectID: new("s1"), + }, + { + Namespace: nspace.Name, + Object: "o2", + Relation: "rel", + SubjectID: new("s2"), + }, + { + Namespace: nspace.Name, + Object: "o3", + Relation: "rel", + SubjectID: new("s3"), + }, + } + testhelpers.MapAndInsertTuples(t, reg, tuples...) + + query := &ketoapi.RelationQuery{ + Namespace: &nspace.Name, + } + firstReq := &rts.ListRelationTuplesRequest{} + enhancer(firstReq, query) + firstReq.PageSize = int32(2) + + firstResp, err := client.ListRelationTuples(ctx, firstReq) + require.NoError(t, err) + + secondReq := &rts.ListRelationTuplesRequest{} + enhancer(secondReq, query) + secondReq.PageSize = int32(2) + secondReq.PageToken = firstResp.NextPageToken + + secondResp, err := client.ListRelationTuples(ctx, secondReq) + require.NoError(t, err) + + assert.Len(t, firstResp.RelationTuples, 2) + assert.Len(t, secondResp.RelationTuples, 1) + assert.Zero(t, secondResp.NextPageToken) + + assert.ElementsMatch(t, tuples, apiTuplesFromProto(t, append(firstResp.RelationTuples, secondResp.RelationTuples...)...)) + }) + }) + } + }) + }) +} diff --git a/internal/relationtuple/swagger_definitions.go b/internal/relationtuple/swagger_definitions.go new file mode 100644 index 0000000..b79906a --- /dev/null +++ b/internal/relationtuple/swagger_definitions.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "github.com/ory/x/pagination/keysetpagination" + + "github.com/ory/keto/ketoapi" +) + +var ( + _ = (*patchRelationships)(nil) + _ = (*getRelationships)(nil) + _ = (*relationshipInQuery)(nil) +) + +// Patch Relationships Request Parameters +// +// swagger:parameters patchRelationships +type patchRelationships struct { + // in:body + Body []*ketoapi.PatchDelta +} + +// Get Relationships Request Parameters +// +// swagger:parameters getRelationships +type getRelationships struct { + // Namespace of the Relationship + // + // in: query + Namespace string `json:"namespace"` + + // Object of the Relationship + // + // in: query + Object string `json:"object"` + + // Relation of the Relationship + // + // in: query + Relation string `json:"relation"` + + // SubjectID of the Relationship + // + // in: query + // Either subject_set.* or subject_id are required. + SubjectID string `json:"subject_id"` + + // Namespace of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SNamespace string `json:"subject_set.namespace"` + + // Object of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SObject string `json:"subject_set.object"` + + // Relation of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SRelation string `json:"subject_set.relation"` + + // swagger:allOf + keysetpagination.RequestParameters +} + +// The relationship parameters in the URL query. +// +// swagger:parameters checkPermission checkPermissionOrError deleteRelationships +type relationshipInQuery struct { + // Namespace of the Relationship + // + // in: query + Namespace string `json:"namespace"` + + // Object of the Relationship + // + // in: query + Object string `json:"object"` + + // Relation of the Relationship + // + // in: query + Relation string `json:"relation"` + + // SubjectID of the Relationship + // + // in: query + // Either subject_set.* or subject_id are required. + SubjectID string `json:"subject_id"` + + // Namespace of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SNamespace string `json:"subject_set.namespace"` + + // Object of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SObject string `json:"subject_set.object"` + + // Relation of the Subject Set + // + // in: query + // Either subject_set.* or subject_id are required. + SRelation string `json:"subject_set.relation"` +} diff --git a/internal/relationtuple/test/manager.go b/internal/relationtuple/test/manager.go new file mode 100644 index 0000000..80f5bec --- /dev/null +++ b/internal/relationtuple/test/manager.go @@ -0,0 +1,803 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "context" + "fmt" + "testing" + + "github.com/gofrs/uuid" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/ketoapi" +) + +func ManagerTest(t *testing.T, m relationtuple.Manager) { + t.Run("method=Write", func(t *testing.T) { + t.Run("case=success", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + tuples := []*relationtuple.RelationTuple{ + tf.Tuple(t, "Obj:o1#rel@s1"), + tf.Tuple(t, "Obj:o2#rel@Obj:o3#sub rel"), + } + + require.NoError(t, m.WriteRelationTuples(t.Context(), tuples...)) + + resp, nextPage, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.True(t, nextPage.IsLast()) + assert.ElementsMatch(t, tuples, resp) + }) + }) + + t.Run("method=Get", func(t *testing.T) { + t.Run("case=queries", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + nspace := tf.NS("Obj") + obj0 := tf.UUID("o0") + + // Tuple i has object o{i%2}, relation "r {i%4}", and subject s{i}. + tuples := make([]*relationtuple.RelationTuple, 10) + for i := range tuples { + tuples[i] = tf.Tuple(t, fmt.Sprintf("Obj:o%d#r %d@s%d", i%2, i%4, i)) + } + + require.NoError(t, m.WriteRelationTuples(t.Context(), tuples...)) + + for i, tc := range []struct { + query *relationtuple.RelationQuery + expected []*relationtuple.RelationTuple + }{ + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + }, + expected: tuples, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Object: &obj0, + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + tuples[2], + tuples[4], + tuples[6], + tuples[8], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Relation: new("r 0"), + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + tuples[4], + tuples[8], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Object: &obj0, + Relation: new("r 0"), + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + tuples[4], + tuples[8], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Subject: tuples[0].Subject, + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Object: &obj0, + Subject: tuples[0].Subject, + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Relation: new("r 0"), + Subject: tuples[0].Subject, + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + }, + }, + { + query: &relationtuple.RelationQuery{ + Namespace: &nspace, + Object: &obj0, + Relation: new("r 0"), + Subject: tuples[0].Subject, + }, + expected: []*relationtuple.RelationTuple{ + tuples[0], + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + res, nextPage, err := m.GetRelationTuples(t.Context(), tc.query) + require.NoError(t, err) + assert.True(t, nextPage.IsLast()) + assert.ElementsMatch(t, tc.expected, res) + }) + } + }) + + t.Run("case=pagination", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + tuples := make([]*relationtuple.RelationTuple, 20) + for i := range tuples { + tuples[i] = tf.Tuple(t, fmt.Sprintf("Obj:o#r@s%d", i)) + } + + require.NoError(t, m.WriteRelationTuples(t.Context(), tuples...)) + + tests := []struct { + name string + searchCriteria *relationtuple.RelationQuery + }{ + { + name: "search=ns,obj,rel", + searchCriteria: &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + Object: new(tf.UUID("o")), + Relation: new("r"), + }, + }, + { + name: "search=ns", + searchCriteria: &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + Object: nil, + Relation: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + res, thisPage []*relationtuple.RelationTuple + err error + ) + nextPage, err := keysetpagination.NewPaginator(keysetpagination.WithSize(1)) + require.NoError(t, err) + for range tuples[:len(tuples)-1] { + thisPage, nextPage, err = m.GetRelationTuples(t.Context(), tt.searchCriteria, nextPage.ToOptions()...) + require.NoError(t, err) + assert.False(t, nextPage.IsLast()) + require.Len(t, thisPage, 1) + + res = append(res, thisPage[0]) + } + + thisPage, nextPage, err = m.GetRelationTuples(t.Context(), tt.searchCriteria, nextPage.ToOptions()...) + require.NoError(t, err) + assert.True(t, nextPage.IsLast()) + require.Len(t, thisPage, 1) + + res = append(res, thisPage[0]) + assert.ElementsMatch(t, tuples, res) + }) + } + }) + + // Comparing paginated results against a non-paginated fetch, + // asserting order and correctness with different page sizes + t.Run("case=pagination returns all rows without skips in correct order", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + // 11 tuples, mixing SubjectID and SubjectSet subjects, including one + // exact duplicate row. + duplicate := tf.Tuple(t, "Obj:o#r@teams:t1#editor") + tuples := []*relationtuple.RelationTuple{ + tf.Tuple(t, "Obj:o#r@groups:g1#member"), + tf.Tuple(t, "Obj:o#r@s1"), + tf.Tuple(t, "Obj:o#r@s2"), + tf.Tuple(t, "Obj:o#r@teams:t2#viewer"), + tf.Tuple(t, "Obj:o#r@groups:g2#owner"), + tf.Tuple(t, "Obj:o#r@s3"), + tf.Tuple(t, "Obj:o#r@orgs:org1#admin"), + tf.Tuple(t, "Obj:o#r@s4"), + duplicate, + duplicate, + tf.Tuple(t, "Obj:o#r@s5"), + } + + require.NoError(t, m.WriteRelationTuples(t.Context(), tuples...)) + + tests := []struct { + name string + searchCriteria *relationtuple.RelationQuery + pageSizes []int + }{ + { + name: "search=ns", + searchCriteria: &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }, + pageSizes: []int{1, 2, 50}, + }, + { + name: "search=obj", + searchCriteria: &relationtuple.RelationQuery{ + Object: new(tf.UUID("o")), + }, + pageSizes: []int{1, 2, 50}, + }, + { + name: "search=ns,obj,rel", + searchCriteria: &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + Object: new(tf.UUID("o")), + Relation: new("r"), + }, + pageSizes: []int{1, 2, 50}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expected, next, err := m.GetRelationTuples(t.Context(), tt.searchCriteria, keysetpagination.WithSize(9999)) + require.NoError(t, err) + require.True(t, next.IsLast()) + require.Len(t, expected, len(tuples)) + + for _, pageSize := range tt.pageSizes { + nextPage, _ := keysetpagination.NewPaginator(keysetpagination.WithSize(pageSize)) + + var got []*relationtuple.RelationTuple + for { + page, np, err := m.GetRelationTuples(t.Context(), tt.searchCriteria, nextPage.ToOptions()...) + require.NoError(t, err) + + require.LessOrEqual(t, len(page), pageSize) + if !np.IsLast() { + require.Len(t, page, pageSize) + } + + got = append(got, page...) + nextPage = np + + if nextPage.IsLast() { + break + } + } + + // Same order as non-paginated fetch. + require.Equal(t, expected, got) + } + }) + } + }) + + t.Run("case=empty list", func(t *testing.T) { + t.Parallel() + nspace := t.Name() + + res, nextPage, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: &nspace, + }) + + assert.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{}, res) + assert.True(t, nextPage.IsLast()) + }) + }) + + t.Run("method=Delete", func(t *testing.T) { + t.Run("case=deletes tuple", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + for _, rt := range []*relationtuple.RelationTuple{ + tf.Tuple(t, "Obj:o#r to delete@s"), + tf.Tuple(t, "Obj:o#r to delete@Obj:o2#r2"), + } { + t.Run(fmt.Sprintf("subject_type=%T", rt.Subject), func(t *testing.T) { + require.NoError(t, m.WriteRelationTuples(t.Context(), rt)) + + res, _, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{rt}, res) + + require.NoError(t, m.DeleteRelationTuples(t.Context(), rt)) + + res, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.Len(t, res, 0) + }) + } + }) + + t.Run("case=deletes only one tuple", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + rs := make([]*relationtuple.RelationTuple, 4) + for i := range rs { + rs[i] = tf.Tuple(t, fmt.Sprintf("Obj:o%d#r%d@s%d", i, i, i)) + } + require.NoError(t, m.WriteRelationTuples(t.Context(), rs...)) + + res, _, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + for _, rt := range rs { + assert.Contains(t, res, rt) + } + + require.NoError(t, m.DeleteRelationTuples(t.Context(), rs[0], rs[2])) + + res, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.ElementsMatch(t, []*relationtuple.RelationTuple{rs[1], rs[3]}, res) + }) + + t.Run("case=tuple and subject namespace differ", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + rt := tf.Tuple(t, "N0:o#r@N1:o#r") + require.NoError(t, m.WriteRelationTuples(t.Context(), rt)) + + actual, _, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: new(tf.NS("N0"))}) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{rt}, actual) + + require.NoError(t, m.DeleteRelationTuples(t.Context(), rt)) + + actual, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: new(tf.NS("N0"))}) + require.NoError(t, err) + assert.Len(t, actual, 0) + }) + }) + + t.Run("method=Transact", func(t *testing.T) { + t.Run("case=success", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + rs := make([]*relationtuple.RelationTuple, 4) + for i := range rs { + rs[i] = tf.Tuple(t, fmt.Sprintf("Obj:o%d#r%d@s%d", i, i, i)) + } + require.NoError(t, m.WriteRelationTuples(t.Context(), rs[0], rs[1])) + + res, _, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.ElementsMatch(t, []*relationtuple.RelationTuple{rs[0], rs[1]}, res) + + require.NoError(t, m.TransactRelationTuples(t.Context(), []*relationtuple.RelationTuple{rs[2], rs[3]}, []*relationtuple.RelationTuple{rs[0]})) + + res, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + + for _, rt := range []*relationtuple.RelationTuple{rs[1], rs[2], rs[3]} { + assert.Contains(t, res, rt) + } + }) + + t.Run("case=err rolls back all", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + + rs := []*relationtuple.RelationTuple{ + tf.Tuple(t, "Obj:o0#r0@s0"), + tf.Tuple(t, "Obj:o1#r1@s1"), + } + invalidRt := &relationtuple.RelationTuple{ + Namespace: tf.NS("Obj"), + Object: tf.UUID("o0"), + Relation: "r0", + Subject: nil, // subject is not allowed to be nil + } + require.NoError(t, m.WriteRelationTuples(t.Context(), rs[0])) + + res, _, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{rs[0]}, res) + + t.Run("invalid=insert", func(t *testing.T) { + assert.ErrorIs(t, m.TransactRelationTuples(t.Context(), []*relationtuple.RelationTuple{invalidRt}, []*relationtuple.RelationTuple{rs[0]}), ketoapi.ErrNilSubject()) + }) + + res, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{rs[0]}, res) + + t.Run("invalid=delete", func(t *testing.T) { + assert.ErrorIs(t, m.TransactRelationTuples(t.Context(), []*relationtuple.RelationTuple{rs[1]}, []*relationtuple.RelationTuple{invalidRt}), ketoapi.ErrNilSubject()) + }) + + res, _, err = m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: new(tf.NS("Obj")), + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{rs[0]}, res) + }) + }) + + traversalTest(t, m) +} + +func traversalTest(t *testing.T, m relationtuple.Manager) { + t.Run("method=FindTupleWithRelations", func(t *testing.T) { + t.Run("case=nil relations returns nil", func(t *testing.T) { + t.Parallel() + result, err := m.FindTupleWithRelations(t.Context(), &relationtuple.RelationTuple{}, nil) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("case=matching relation returns tuple", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + tuple := tf.Tuple(t, "Obj:o#viewer@User:u") + require.NoError(t, m.WriteRelationTuples(t.Context(), tuple)) + + // The query tuple's own relation is ignored; only the relations list matters. + result, err := m.FindTupleWithRelations(t.Context(), tf.Tuple(t, "Obj:o#editor@User:u"), []string{"viewer"}) + require.NoError(t, err) + assert.Equal(t, tuple, result) + }) + + t.Run("case=non-matching relation returns nil", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + tuple := tf.Tuple(t, "Obj:o#viewer@u") + require.NoError(t, m.WriteRelationTuples(t.Context(), tuple)) + + result, err := m.FindTupleWithRelations(t.Context(), tuple, []string{"editor"}) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("case=multiple relations with one matching returns tuple", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + tuple := tf.Tuple(t, "Obj:o#viewer@u") + require.NoError(t, m.WriteRelationTuples(t.Context(), tuple)) + + result, err := m.FindTupleWithRelations(t.Context(), tuple, []string{"editor", "viewer", "owner"}) + require.NoError(t, err) + assert.Equal(t, tuple, result) + }) + }) + + t.Run("method=TraverseSubjectSetExpansion", func(t *testing.T) { + t.Run("case=no subject set tuples returns empty", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + res, err := m.TraverseSubjectSetExpansion(t.Context(), tf.Tuple(t, "Obj:o#rel@u"), nil) + require.NoError(t, err) + assert.Empty(t, res) + }) + + t.Run("case=subject found in subject set", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + membership := tf.Tuple(t, "Group:g#member@u") + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Group:g#member"), + membership, + )) + + start := tf.Tuple(t, "Obj:o#rel@u") + res, err := m.TraverseSubjectSetExpansion(t.Context(), start, nil) + require.NoError(t, err) + require.Len(t, res, 1) + assert.True(t, res[0].Found) + assert.Equal(t, relationtuple.TraversalSubjectSetExpand, res[0].Via) + assert.Equal(t, start, res[0].From) + assert.Equal(t, membership.Namespace, res[0].To.Namespace) + assert.Equal(t, membership.Object, res[0].To.Object) + assert.Equal(t, membership.Relation, res[0].To.Relation) + }) + + t.Run("case=subject not found in subject set", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + // Subject-set pointer without a membership tuple behind it. + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Group:g#member"), + )) + + res, err := m.TraverseSubjectSetExpansion(t.Context(), tf.Tuple(t, "Obj:o#rel@u"), nil) + require.NoError(t, err) + require.Len(t, res, 1) + assert.False(t, res[0].Found) + }) + + t.Run("case=allowedSubjectSets filters out non-matching types", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Doc:d#member"), + tf.Tuple(t, "Obj:o#rel@Group:g#admin"), + )) + + res, err := m.TraverseSubjectSetExpansion(t.Context(), tf.Tuple(t, "Obj:o#rel@u"), []relationtuple.SubjectSetType{{Namespace: tf.NS("Doc"), Relation: "member"}}) + require.NoError(t, err) + require.Len(t, res, 1) + assert.Equal(t, tf.NS("Doc"), res[0].To.Namespace) + assert.Equal(t, "member", res[0].To.Relation) + }) + + t.Run("case=nil allowedSubjectSets returns all types", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Doc:d#member"), + tf.Tuple(t, "Obj:o#rel@Group:g#admin"), + )) + + res, err := m.TraverseSubjectSetExpansion(t.Context(), tf.Tuple(t, "Obj:o#rel@u"), nil) + require.NoError(t, err) + assert.Len(t, res, 2) + }) + + t.Run("case=AllowsDirect=false direct relationship is not checked", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Group:g#member"), + tf.Tuple(t, "Group:g#member@User:u"), + )) + + start := tf.Tuple(t, "Obj:o#rel@User:u") + res, err := m.TraverseSubjectSetExpansion(t.Context(), start, []relationtuple.SubjectSetType{{Namespace: tf.NS("Group"), Relation: "member", AllowsDirect: false}}) + require.NoError(t, err) + require.Len(t, res, 1) + assert.False(t, res[0].Found) + }) + + t.Run("case=AllowsDirect gates EXISTS per type", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Allowed:a#member"), + tf.Tuple(t, "Obj:o#rel@NotAllowed:n#member"), + tf.Tuple(t, "Allowed:a#member@User:u"), + tf.Tuple(t, "NotAllowed:n#member@User:u"), + )) + + start := tf.Tuple(t, "Obj:o#rel@User:u") + + t.Run("non-allowed type returns found=false", func(t *testing.T) { + t.Parallel() + res, err := m.TraverseSubjectSetExpansion(t.Context(), start, + []relationtuple.SubjectSetType{{Namespace: tf.NS("NotAllowed"), Relation: "member", AllowsDirect: false}}, + ) + require.NoError(t, err) + require.Len(t, res, 1) + assert.Equal(t, tf.NS("NotAllowed"), res[0].To.Namespace) + assert.False(t, res[0].Found) + }) + + t.Run("mixed: allowed returns found=true, non-allowed returns found=false", func(t *testing.T) { + t.Parallel() + res, err := m.TraverseSubjectSetExpansion(t.Context(), start, []relationtuple.SubjectSetType{ + {Namespace: tf.NS("Allowed"), Relation: "member", AllowsDirect: true}, + {Namespace: tf.NS("NotAllowed"), Relation: "member", AllowsDirect: false}, + }) + require.NoError(t, err) + // TraverseSubjectSetExpansion can short-circuit if it reads the allowed + // namespace first, returning only 1 item. + require.GreaterOrEqual(t, len(res), 1) + + byNs := make(map[string]*relationtuple.TraversalResult) + for _, r := range res { + byNs[r.To.Namespace] = r + } + assert.True(t, byNs[tf.NS("Allowed")].Found) + }) + }) + + t.Run("case=multiple AllowsDirect types are checked in one query", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@GroupA:a#member"), + tf.Tuple(t, "Obj:o#rel@GroupB:b#member"), + tf.Tuple(t, "Obj:o#rel@Stale:s#member"), + // A stale membership under the type that does not allow direct matches. + tf.Tuple(t, "Stale:s#member@User:u"), + )) + + start := tf.Tuple(t, "Obj:o#rel@User:u") + types := []relationtuple.SubjectSetType{ + {Namespace: tf.NS("GroupA"), Relation: "member", AllowsDirect: true}, + {Namespace: tf.NS("GroupB"), Relation: "member", AllowsDirect: true}, + {Namespace: tf.NS("Stale"), Relation: "member", AllowsDirect: false}, + } + + // Nothing is found, so the traversal cannot short-circuit and must + // return all three pointers with found=false. + res, err := m.TraverseSubjectSetExpansion(t.Context(), start, types) + require.NoError(t, err) + require.Len(t, res, 3) + for _, r := range res { + assert.False(t, r.Found, "namespace %s must not be found", r.To.Namespace) + } + }) + + t.Run("case=multiple allowedSubjectSets entries each match independently", func(t *testing.T) { + t.Parallel() + tf := testhelpers.NewTupleFactory() + require.NoError(t, m.WriteRelationTuples(t.Context(), + tf.Tuple(t, "Obj:o#rel@Doc:d#member"), + tf.Tuple(t, "Obj:o#rel@Group:g#admin"), + // Not in the allowed list below. + tf.Tuple(t, "Obj:o#rel@Group:g2#viewer"), + )) + + res, err := m.TraverseSubjectSetExpansion(t.Context(), tf.Tuple(t, "Obj:o#rel@u"), []relationtuple.SubjectSetType{ + {Namespace: tf.NS("Doc"), Relation: "member"}, + {Namespace: tf.NS("Group"), Relation: "admin"}, + }) + require.NoError(t, err) + assert.Len(t, res, 2) + }) + }) +} + +func twice(t *testing.T, m0, m1 relationtuple.Manager) func(string, func(*testing.T, relationtuple.Manager, relationtuple.Manager)) { + return func(desc string, run func(*testing.T, relationtuple.Manager, relationtuple.Manager)) { + t.Run(desc+"-0", func(t *testing.T) { + run(t, m0, m1) + }) + t.Run(desc+"-1", func(t *testing.T) { + run(t, m1, m0) + }) + } +} + +func reset(t *testing.T, ms ...relationtuple.Manager) { + for _, m := range ms { + for { + rts, next, err := m.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{}) + require.NoError(t, err) + + require.NoError(t, m.DeleteRelationTuples(t.Context(), rts...)) + + if next.IsLast() { + break + } + } + } +} + +func IsolationTest(t *testing.T, m0, m1 relationtuple.Manager) { + run := twice(t, m0, m1) + + run("suite=lifecycle", func(t *testing.T, m0, m1 relationtuple.Manager) { + nspace := t.Name() + + rts := []*relationtuple.RelationTuple{ + { + Namespace: nspace, + Object: uuid.Must(uuid.NewV4()), + Relation: "r", + Subject: &relationtuple.SubjectSet{ + Namespace: nspace, + Object: uuid.Must(uuid.NewV4()), + Relation: "r", + }, + }, + { + Namespace: nspace, + Object: uuid.Must(uuid.NewV4()), + Relation: "r", + Subject: &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}, + }, + } + + t.Run("case=write and get", func(t *testing.T) { + reset(t, m0, m1) + + require.NoError(t, m0.WriteRelationTuples(t.Context(), rts...)) + + other, _, err := m1.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + assert.Len(t, other, 0) + + actual, _, err := m0.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + assert.ElementsMatch(t, rts, actual) + }) + + t.Run("case=delete", func(t *testing.T) { + reset(t, m0, m1) + + require.NoError(t, m1.WriteRelationTuples(t.Context(), rts...)) + + require.NoError(t, m0.DeleteRelationTuples(t.Context(), rts...)) + + deleted, _, err := m0.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + assert.Len(t, deleted, 0) + + actual, _, err := m1.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + assert.ElementsMatch(t, rts, actual) + }) + + t.Run("case=transact", func(t *testing.T) { + reset(t, m0, m1) + + // note that the reset is outside this subtest, so in the second run we actually delete what we had before + twice(t, m0, m1)("insert and delete", func(t *testing.T, m0, m1 relationtuple.Manager) { + require.NoError(t, m0.TransactRelationTuples(t.Context(), []*relationtuple.RelationTuple{rts[0]}, []*relationtuple.RelationTuple{rts[1]})) + + require.NoError(t, m1.TransactRelationTuples(t.Context(), []*relationtuple.RelationTuple{rts[1]}, []*relationtuple.RelationTuple{rts[0]})) + + r0, _, err := m0.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + + r1, _, err := m1.GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace}) + require.NoError(t, err) + + assert.Equal(t, rts[:1], r0) + assert.Equal(t, rts[1:], r1) + }) + }) + + t.Run("case=cancelled", func(t *testing.T) { + reset(t, m0, m1) + ctx, cancel := context.WithCancel(t.Context()) + + require.NoError(t, m0.WriteRelationTuples(ctx, rts...)) + + cancel() + + _, _, err := m0.GetRelationTuples(ctx, &relationtuple.RelationQuery{Namespace: &nspace}) + assert.ErrorIs(t, err, context.Canceled) + }) + }) +} diff --git a/internal/relationtuple/test/uuid_mapping.go b/internal/relationtuple/test/uuid_mapping.go new file mode 100644 index 0000000..7229235 --- /dev/null +++ b/internal/relationtuple/test/uuid_mapping.go @@ -0,0 +1,53 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package test + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" +) + +func MappingManagerTest(t *testing.T, m relationtuple.MappingManager) { + t.Run("case=str -> uuid -> str", func(t *testing.T) { + const s = "rep1" + u, err := m.MapStringsToUUIDs(t.Context(), s) + require.NoError(t, err) + + actual, err := m.MapUUIDsToStrings(t.Context(), u[0]) + require.NoError(t, err) + require.Len(t, actual, 1) + assert.Equal(t, s, actual[0]) + + t.Run("case=batch", func(t *testing.T) { + s := []string{"rep1", "rep2", "rep3"} + + u, err := m.MapStringsToUUIDs(t.Context(), s...) + require.NoError(t, err) + require.Len(t, u, len(s)) + + assert.NotContains(t, u, uuid.Nil) + + actual, err := m.MapUUIDsToStrings(t.Context(), u...) + require.NoError(t, err) + require.Len(t, actual, len(s)) + assert.Equal(t, s, actual) + }) + }) + + t.Run("case=deterministic MapStringsToUUIDs", func(t *testing.T) { + const s = "some string" + + u0, err := m.MapStringsToUUIDs(t.Context(), s) + require.NoError(t, err) + u1, err := m.MapStringsToUUIDs(t.Context(), s) + require.NoError(t, err) + + assert.Equal(t, u0, u1) + }) +} diff --git a/internal/relationtuple/transact_server.go b/internal/relationtuple/transact_server.go new file mode 100644 index 0000000..9b47f2a --- /dev/null +++ b/internal/relationtuple/transact_server.go @@ -0,0 +1,320 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "context" + "encoding/json" + "net/http" + + "connectrpc.com/connect" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/herodot" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" + "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2/relationtuplesconnect" + "github.com/ory/keto/internal/x/validate" + "github.com/ory/keto/ketoapi" + "github.com/ory/keto/x/events" +) + +var _ relationtuplesconnect.WriteServiceHandler = (*WriteHandler)(nil) + +func protoTuplesWithAction(deltas []*rts.RelationTupleDelta, action rts.RelationTupleDelta_Action) (filtered []*ketoapi.RelationTuple, err error) { + for _, d := range deltas { + if d.Action == action { + it, err := (&ketoapi.RelationTuple{}).FromDataProvider(d.RelationTuple) + if err != nil { + return nil, err + } + filtered = append(filtered, it) + } + } + return +} + +func (h *WriteHandler) TransactRelationTuples(ctx context.Context, req *connect.Request[rts.TransactRelationTuplesRequest]) (*connect.Response[rts.TransactRelationTuplesResponse], error) { + insertTuples, err := protoTuplesWithAction(req.Msg.RelationTupleDeltas, rts.RelationTupleDelta_ACTION_INSERT) + if err != nil { + return nil, err + } + + deleteTuples, err := protoTuplesWithAction(req.Msg.RelationTupleDeltas, rts.RelationTupleDelta_ACTION_DELETE) + if err != nil { + return nil, err + } + + err = h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error { + its, err := h.d.Mapper().FromTuple(ctx, append(insertTuples, deleteTuples...)...) + if err != nil { + return err + } + return h.d.RelationTupleManager().TransactRelationTuples(ctx, its[:len(insertTuples)], its[len(insertTuples):]) + }) + + if err != nil { + return nil, err + } + + trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesChanged(ctx)) + + snaptokens := make([]string, len(insertTuples)) + for i := range insertTuples { + snaptokens[i] = "not yet implemented" + } + return connect.NewResponse(&rts.TransactRelationTuplesResponse{ + Snaptokens: snaptokens, + }), nil +} + +func (h *WriteHandler) DeleteRelationTuples(ctx context.Context, req *connect.Request[rts.DeleteRelationTuplesRequest]) (*connect.Response[rts.DeleteRelationTuplesResponse], error) { + var q ketoapi.RelationQuery + + switch { + case req.Msg.RelationQuery != nil: + q.FromDataProvider(&queryWrapper{req.Msg.RelationQuery}) + //lint:ignore SA1019 required for compatibility + case req.Msg.Query != nil: //nolint:staticcheck + //lint:ignore SA1019 backwards compatibility + q.FromDataProvider(&deprecatedQueryWrapper{(*rts.ListRelationTuplesRequest_Query)(req.Msg.Query)}) //nolint:staticcheck + default: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("invalid request")) + } + + iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, &q) + if err != nil { + return nil, err + } + if err := h.d.RelationTupleManager().DeleteAllRelationTuples(ctx, iq); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithError(err.Error())) + } + + trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesDeleted(ctx)) + + return connect.NewResponse(&rts.DeleteRelationTuplesResponse{}), nil +} + +// Create Relationship Request Parameters +// +// swagger:parameters createRelationship +// +//lint:ignore U1000 required for OpenAPI +type createRelationship struct { + // in: body + Body createRelationshipBody +} + +// Create Relationship Request Body +// +// swagger:model createRelationshipBody +type createRelationshipBody struct { + ketoapi.RelationQuery +} + +// swagger:route PUT /admin/relation-tuples relationship createRelationship +// +// # Create a Relationship +// +// Use this endpoint to create a relationship. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 201: relationship +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-low +func (h *WriteHandler) createRelation(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var rt ketoapi.RelationTuple + if err := json.NewDecoder(r.Body).Decode(&rt); err != nil { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()))) + return + } + + if err := rt.Validate(); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + h.d.Logger().WithFields(rt.ToLoggerFields()).Debug("creating relation tuple") + + err := h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error { + it, err := h.d.Mapper().FromTuple(ctx, &rt) + if err != nil { + h.d.Logger().WithError(err).WithFields(rt.ToLoggerFields()).Errorf("could not map relation tuple to UUIDs") + return err + } + if err := h.d.RelationTupleManager().WriteRelationTuples(ctx, it...); err != nil { + h.d.Logger().WithError(err).WithFields(rt.ToLoggerFields()).Errorf("got an error while creating the relation tuple") + return err + } + return nil + }) + + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesCreated(ctx)) + + h.d.Writer().WriteCreated(w, r, + ReadRouteBase+"?"+rt.ToURLQuery().Encode(), + &rt, + ) +} + +// swagger:route DELETE /admin/relation-tuples relationship deleteRelationships +// +// # Delete Relationships +// +// Use this endpoint to delete relationships +// +// Consumes: +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-low +func (h *WriteHandler) deleteRelations(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if err := validate.All(r, + validate.NoExtraQueryParams(ketoapi.RelationQueryKeys...), + validate.QueryParamsContainsOneOf(ketoapi.NamespaceKey), + validate.HasEmptyBody(), + ); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + q := r.URL.Query() + query, err := (&ketoapi.RelationQuery{}).FromURLQuery(q) + if err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error())) + return + } + + l := h.d.Logger() + for k := range q { + l = l.WithField(k, q.Get(k)) + } + l.Debug("deleting relationships") + + iq, err := h.d.ReadOnlyMapper().FromQuery(ctx, query) + if err != nil { + h.d.Logger().WithError(err).Errorf("could not map fields to UUIDs") + h.d.Writer().WriteError(w, r, err) + return + } + if err := h.d.RelationTupleManager().DeleteAllRelationTuples(ctx, iq); err != nil { + l.WithError(err).Errorf("got an error while deleting relationships") + h.d.Writer().WriteError(w, r, herodot.ErrInternalServerError().WithError(err.Error())) + return + } + + trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesDeleted(ctx)) + + w.WriteHeader(http.StatusNoContent) +} + +func internalTuplesWithAction(deltas []*ketoapi.PatchDelta, action ketoapi.PatchAction) (filtered []*ketoapi.RelationTuple) { + for _, d := range deltas { + if d.Action == action { + filtered = append(filtered, d.RelationTuple) + } + } + return +} + +// swagger:route PATCH /admin/relation-tuples relationship patchRelationships +// +// # Patch Multiple Relationships +// +// Use this endpoint to patch one or more relationships. +// +// Consumes: +// - application/json +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 204: emptyResponse +// 400: errorGeneric +// 404: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-low +func (h *WriteHandler) patchRelationTuples(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var deltas []*ketoapi.PatchDelta + if err := json.NewDecoder(r.Body).Decode(&deltas); err != nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError(err.Error())) + return + } + for _, d := range deltas { + if d.RelationTuple == nil { + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("relation_tuple is missing")) + return + } + if err := d.RelationTuple.Validate(); err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + switch d.Action { + case ketoapi.ActionInsert, ketoapi.ActionDelete: + default: + h.d.Writer().WriteError(w, r, herodot.ErrBadRequest().WithError("unknown action "+string(d.Action))) + return + } + } + + insertTuples := internalTuplesWithAction(deltas, ketoapi.ActionInsert) + deleteTuples := internalTuplesWithAction(deltas, ketoapi.ActionDelete) + + err := h.d.RelationTupleManager().Transaction(ctx, func(ctx context.Context) error { + its, err := h.d.Mapper().FromTuple(ctx, append(insertTuples, deleteTuples...)...) + if err != nil { + h.d.Logger().WithError(err).Errorf("got an error while mapping fields to UUID") + return err + } + return h.d.RelationTupleManager().TransactRelationTuples(ctx, its[:len(insertTuples)], its[len(insertTuples):]) + }) + + if err != nil { + h.d.Writer().WriteError(w, r, err) + return + } + + trace.SpanFromContext(ctx).AddEvent(events.NewRelationtuplesChanged(ctx)) + + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/relationtuple/transact_server_test.go b/internal/relationtuple/transact_server_test.go new file mode 100644 index 0000000..7eb32d5 --- /dev/null +++ b/internal/relationtuple/transact_server_test.go @@ -0,0 +1,486 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple_test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/ory/x/httprouterx" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/internal/testhelpers" + "github.com/ory/keto/ketoapi" +) + +func TestWriteHandlers(t *testing.T) { + r := httprouterx.NewRouterAdmin() + reg := driver.NewSqliteTestRegistry(t) + + var nspaces []*namespace.Namespace + addNamespace := func(t *testing.T) *namespace.Namespace { + n := &namespace.Namespace{ + Name: t.Name(), + } + nspaces = append(nspaces, n) + + require.NoError(t, reg.Config(t.Context()).Set(config.KeyNamespaces, nspaces)) + + return n + } + + relationtuple.NewWriteHandler(reg).RegisterWriteRoutes(r) + relationtuple.NewReadHandler(reg).RegisterReadRoutes(r.ToPublic()) + ts := httptest.NewServer(r) + defer ts.Close() + + t.Run("method=create", func(t *testing.T) { + doCreate := func(raw []byte) *http.Response { + req, err := http.NewRequest(http.MethodPut, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(raw)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + + return resp + } + + t.Run("case=creates tuple", func(t *testing.T) { + nspace := addNamespace(t) + + rt := &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "obj", + Relation: "rel", + SubjectID: new("subj"), + } + payload, err := json.Marshal(rt) + require.NoError(t, err) + + resp := doCreate(payload) + + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + body, err := io.ReadAll(resp.Body) + require.NoError(t, err) + + assert.JSONEq(t, string(payload), string(body)) + + t.Run("check=is contained in the manager", func(t *testing.T) { + mapped, err := reg.Mapper().FromTuple(t.Context(), rt) + require.NoError(t, err) + // set a size > 1 just to make sure it gets all + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mapped[0].ToQuery(), keysetpagination.WithSize(10)) + require.NoError(t, err) + actual, err := reg.Mapper().ToTuple(t.Context(), actualRTs...) + require.NoError(t, err) + assert.Equalf(t, []*ketoapi.RelationTuple{rt}, actual, "want: %s\ngot: %s", rt.String(), actual[0].String()) + }) + + t.Run("check=is gettable with the returned URL", func(t *testing.T) { + resp, err := ts.Client().Get(ts.URL + resp.Header.Get("Location")) + require.NoError(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + + respDec := ketoapi.GetResponse{} + require.NoError(t, json.NewDecoder(resp.Body).Decode(&respDec)) + assert.Equal(t, []*ketoapi.RelationTuple{rt}, respDec.RelationTuples) + }) + }) + + t.Run("case=returns bad request on JSON parse error", func(t *testing.T) { + resp := doCreate([]byte("foo")) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("case=special chars", func(t *testing.T) { + nspace := addNamespace(t) + + rts := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "group:B", + Relation: "member", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "group:A", + Relation: "member", + }, + }, + { + Namespace: nspace.Name, + Object: "@all", + Relation: "member", + SubjectID: new("this:could#be interpreted:as a@subject set"), + }, + } + + for _, rt := range rts { + payload, err := json.Marshal(rt) + require.NoError(t, err) + + resp := doCreate(payload) + assert.Equal(t, http.StatusCreated, resp.StatusCode) + } + + actual, next, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: &nspace.Name, + }) + require.NoError(t, err) + actualMapped, err := reg.Mapper().ToTuple(t.Context(), actual...) + require.NoError(t, err) + assert.True(t, next.IsLast()) + assert.ElementsMatch(t, rts, actualMapped) + }) + }) + + t.Run("method=delete", func(t *testing.T) { + t.Run("case=deletes a tuple", func(t *testing.T) { + nspace := addNamespace(t) + + rt := &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "deleted obj", + Relation: "deleted rel", + SubjectID: new("deleted subj"), + } + testhelpers.MapAndInsertTuples(t, reg, rt) + + req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.WriteRouteBase+"?"+rt.ToURLQuery().Encode(), nil) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + // set a size > 1 just to make sure it gets all + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace.Name}, keysetpagination.WithSize(10)) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs) + }) + + t.Run("case=deletes multiple tuples", func(t *testing.T) { + nspace := addNamespace(t) + + rts := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "deleted obj", + Relation: "deleted rel", + SubjectID: new("deleted subj 1"), + }, + { + Namespace: nspace.Name, + Object: "deleted obj", + Relation: "deleted rel", + SubjectID: new("deleted subj 2"), + }, + } + + testhelpers.MapAndInsertTuples(t, reg, rts...) + + q := url.Values{ + "namespace": {nspace.Name}, + "object": {"deleted obj"}, + "relation": {"deleted rel"}, + } + req, err := http.NewRequest(http.MethodDelete, ts.URL+relationtuple.WriteRouteBase+"?"+q.Encode(), nil) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + query, err := (&ketoapi.RelationQuery{}).FromURLQuery(q) + require.NoError(t, err) + mappedQuery, err := reg.Mapper().FromQuery(t.Context(), query) + require.NoError(t, err) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mappedQuery, keysetpagination.WithSize(10)) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs) + }) + + t.Run("suite=bad requests", func(t *testing.T) { + nspace := addNamespace(t) + + rts := []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "deleted obj", + Relation: "deleted rel", + SubjectID: new("deleted subj 1"), + }, + { + Namespace: nspace.Name, + Object: "deleted obj", + Relation: "deleted rel", + SubjectID: new("deleted subj 2"), + }, + } + + testhelpers.MapAndInsertTuples(t, reg, rts...) + + assertBadRequest := func(t *testing.T, req *http.Request) { + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + } + + assertTuplesExist := func(t *testing.T) { + mappedQuery, err := reg.Mapper().FromQuery(t.Context(), &ketoapi.RelationQuery{ + Namespace: &nspace.Name, + }) + require.NoError(t, err) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), mappedQuery, keysetpagination.WithSize(10)) + require.NoError(t, err) + mappedRTs, err := reg.Mapper().ToTuple(t.Context(), actualRTs...) + require.NoError(t, err) + assert.ElementsMatch(t, rts, mappedRTs) + } + + t.Run("case=bad request if body sent", func(t *testing.T) { + q := url.Values{ + "namespace": {nspace.Name}, + "object": {"deleted obj"}, + "relation": {"deleted rel"}, + } + req, err := http.NewRequest( + http.MethodDelete, + ts.URL+relationtuple.WriteRouteBase+"?"+q.Encode(), + strings.NewReader("some body")) + require.NoError(t, err) + + assertBadRequest(t, req) + assertTuplesExist(t) + }) + + t.Run("case=bad request query param misspelled", func(t *testing.T) { + req, err := http.NewRequest( + http.MethodDelete, + ts.URL+relationtuple.WriteRouteBase+"?invalid=param", + nil) + require.NoError(t, err) + + assertBadRequest(t, req) + assertTuplesExist(t) + }) + + t.Run("case=bad request if query params misssing", func(t *testing.T) { + req, err := http.NewRequest( + http.MethodDelete, + ts.URL+relationtuple.WriteRouteBase, + nil) + require.NoError(t, err) + + assertBadRequest(t, req) + assertTuplesExist(t) + }) + }) + }) + + t.Run("method=patch", func(t *testing.T) { + t.Run("case=create and delete", func(t *testing.T) { + nspace := addNamespace(t) + relation := t.Name() + + deltas := []*ketoapi.PatchDelta{ + { + Action: ketoapi.ActionInsert, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "create obj", + Relation: relation, + SubjectID: new("create sub"), + }, + }, + { + Action: ketoapi.ActionDelete, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "delete obj", + Relation: relation, + SubjectID: new("delete sub"), + }, + }, + } + testhelpers.MapAndInsertTuples(t, reg, deltas[1].RelationTuple) + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: &nspace.Name, + Relation: &relation, + }) + require.NoError(t, err) + mapped, err := reg.Mapper().ToTuple(t.Context(), actualRTs...) + require.NoError(t, err) + assert.Equal(t, []*ketoapi.RelationTuple{deltas[0].RelationTuple}, mapped) + }) + + t.Run("case=ignores rest on err", func(t *testing.T) { + nspace := addNamespace(t) + + deltas := []*ketoapi.PatchDelta{ + { + Action: ketoapi.ActionInsert, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "create obj", + Relation: t.Name(), + SubjectID: new("create sub"), + }, + }, + { + Action: ketoapi.ActionDelete, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: "not " + nspace.Name, + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + }, + } + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNotFound, resp.StatusCode) + + // set a size > 1 just to make sure it gets all + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{Namespace: &nspace.Name}, keysetpagination.WithSize(10)) + require.NoError(t, err) + assert.Len(t, actualRTs, 0) + }) + + t.Run("case=only create", func(t *testing.T) { + nspace := addNamespace(t) + + deltas := []*ketoapi.PatchDelta{ + { + Action: ketoapi.ActionInsert, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "create obj", + Relation: "rel", + SubjectID: new("create sub"), + }, + }, + } + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: &nspace.Name, + }) + require.NoError(t, err) + mapped, err := reg.Mapper().ToTuple(t.Context(), actualRTs...) + require.NoError(t, err) + assert.Equal(t, []*ketoapi.RelationTuple{deltas[0].RelationTuple}, mapped) + }) + + t.Run("case=only delete", func(t *testing.T) { + nspace := addNamespace(t) + + deltas := []*ketoapi.PatchDelta{ + { + Action: ketoapi.ActionDelete, + RelationTuple: &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: "delete obj", + Relation: "rel", + SubjectID: new("delete sub"), + }, + }, + } + + body, err := json.Marshal(deltas) + require.NoError(t, err) + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBuffer(body)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusNoContent, resp.StatusCode) + + actualRTs, _, err := reg.RelationTupleManager().GetRelationTuples(t.Context(), &relationtuple.RelationQuery{ + Namespace: &nspace.Name, + }) + require.NoError(t, err) + assert.Equal(t, []*relationtuple.RelationTuple{}, actualRTs) + }) + + t.Run("case=valid JSON, invalid content", func(t *testing.T) { + rawJSON := ` +[ + { + "action": "insert", + "namespace":"role", + "object":"super-admin", + "relation":"member", + "subject":"role:company-admin" + } +]` + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBufferString(rawJSON)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + defer func() { _ = resp.Body.Close() }() + errContent, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(errContent), "relation_tuple is missing") + }) + + t.Run("case=unknown action", func(t *testing.T) { + rawJSON := ` +[ + { + "action": "unknown_action_foo", + "relation_tuple": { + "namespace":"role", + "object":"super-admin", + "relation":"member", + "subject_id":"role:company-admin" + } + } +]` + req, err := http.NewRequest(http.MethodPatch, ts.URL+relationtuple.WriteRouteBase, bytes.NewBufferString(rawJSON)) + require.NoError(t, err) + resp, err := ts.Client().Do(req) //nolint:gosec // Test code with controlled URL. + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + + defer func() { _ = resp.Body.Close() }() + errContent, err := io.ReadAll(resp.Body) + require.NoError(t, err) + assert.Contains(t, string(errContent), "unknown_action_foo") + }) + }) +} diff --git a/internal/relationtuple/uuid_mapping.go b/internal/relationtuple/uuid_mapping.go new file mode 100644 index 0000000..20e7e44 --- /dev/null +++ b/internal/relationtuple/uuid_mapping.go @@ -0,0 +1,397 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple + +import ( + "context" + + "github.com/ory/x/otelx" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/ketoapi" + + "github.com/gofrs/uuid" +) + +type ( + mapperDependencies interface { + MappingManagerProvider + config.Provider + } + MappingManagerProvider interface { + MappingManager() MappingManager + } + MappingManager interface { + MapStringsToUUIDs(ctx context.Context, s ...string) ([]uuid.UUID, error) + MapStringsToUUIDsReadOnly(ctx context.Context, s ...string) ([]uuid.UUID, error) + MapUUIDsToStrings(ctx context.Context, u ...uuid.UUID) ([]string, error) + } + MapperProvider interface { + Mapper() *Mapper + ReadOnlyMapper() *Mapper + } + Mapper struct { + D mapperDependencies + ReadOnly bool + } +) + +type success struct { + fs []func() + err *error +} + +func newSuccess(err *error) *success { + return &success{ + err: err, + } +} + +func (c *success) do(f func()) { + c.fs = append(c.fs, f) +} + +func (c *success) apply() { + if *c.err != nil { + return + } + for _, f := range c.fs { + f() + } +} + +func (m *Mapper) FromQuery(ctx context.Context, apiQuery *ketoapi.RelationQuery) (res *RelationQuery, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.FromQuery") + defer otelx.End(span, &err) + + onSuccess := newSuccess(&err) + defer onSuccess.apply() + + var s []string + var u []uuid.UUID + res = &RelationQuery{ + Relation: apiQuery.Relation, + } + + nm, err := m.D.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + if apiQuery.Namespace != nil { + n, err := nm.GetNamespaceByName(ctx, *apiQuery.Namespace) + if err != nil { + return nil, err + } + res.Namespace = new(n.Name) + } + if apiQuery.Object != nil { + s = append(s, *apiQuery.Object) + onSuccess.do(func(i int) func() { + return func() { + res.Object = new(u[i]) + } + }(len(s) - 1)) + } + if apiQuery.SubjectID != nil { + s = append(s, *apiQuery.SubjectID) + onSuccess.do(func(i int) func() { + return func() { + res.Subject = &SubjectID{u[i]} + } + }(len(s) - 1)) + } + if apiQuery.SubjectSet != nil { + s = append(s, apiQuery.SubjectSet.Object) + n, err := nm.GetNamespaceByName(ctx, apiQuery.SubjectSet.Namespace) + if err != nil { + return nil, err + } + onSuccess.do(func(i int) func() { + return func() { + res.Subject = &SubjectSet{ + Namespace: n.Name, + Object: u[i], + Relation: apiQuery.SubjectSet.Relation, + } + } + }(len(s) - 1)) + } + + if m.ReadOnly { + u, err = m.D.MappingManager().MapStringsToUUIDsReadOnly(ctx, s...) + } else { + u, err = m.D.MappingManager().MapStringsToUUIDs(ctx, s...) + } + if err != nil { + return nil, err + } + return res, nil +} + +func (m *Mapper) ToQuery(ctx context.Context, q *RelationQuery) (res *ketoapi.RelationQuery, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.ToQuery") + defer otelx.End(span, &err) + + onSuccess := newSuccess(&err) + defer onSuccess.apply() + + var s []string + var u []uuid.UUID + res = &ketoapi.RelationQuery{ + Relation: q.Relation, + } + + nm, err := m.D.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + if q.Namespace != nil { + n, err := nm.GetNamespaceByName(ctx, *q.Namespace) + if err != nil { + return nil, err + } + res.Namespace = new(n.Name) + } + if q.Object != nil { + u = append(u, *q.Object) + onSuccess.do(func() { + res.Object = new(s[0]) + }) + } + if q.Subject != nil { + switch sub := q.Subject.(type) { + case *SubjectID: + u = append(u, sub.ID) + onSuccess.do(func() { + res.SubjectID = new(s[len(s)-1]) + }) + case *SubjectSet: + u = append(u, sub.Object) + n, err := nm.GetNamespaceByName(ctx, sub.Namespace) + if err != nil { + return nil, err + } + onSuccess.do(func() { + res.SubjectSet = &ketoapi.SubjectSet{ + Namespace: n.Name, + Object: s[len(s)-1], + Relation: sub.Relation, + } + }) + } + } + + s, err = m.D.MappingManager().MapUUIDsToStrings(ctx, u...) + if err != nil { + return nil, err + } + return res, nil +} + +func (m *Mapper) FromTuple(ctx context.Context, ts ...*ketoapi.RelationTuple) (res []*RelationTuple, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.FromTuple") + defer otelx.End(span, &err) + + onSuccess := newSuccess(&err) + defer onSuccess.apply() + + res = make([]*RelationTuple, 0, len(ts)) + s := make([]string, 0, len(ts)*2) + var u []uuid.UUID + + nm, err := m.D.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + + for _, t := range ts { + t := t + n, err := nm.GetNamespaceByName(ctx, t.Namespace) + if err != nil { + return nil, err + } + mt := RelationTuple{ + Namespace: n.Name, + Relation: t.Relation, + } + i := len(res) + + if err := t.Validate(); err != nil { + return nil, err + } + if t.SubjectID != nil { + s = append(s, *t.SubjectID) + onSuccess.do(func() { + mt.Subject = &SubjectID{u[i*2]} + }) + } else if t.SubjectSet != nil { + n, err := nm.GetNamespaceByName(ctx, t.SubjectSet.Namespace) + if err != nil { + return nil, err + } + s = append(s, t.SubjectSet.Object) + onSuccess.do(func() { + mt.Subject = &SubjectSet{ + Namespace: n.Name, + Object: u[i*2], + Relation: t.SubjectSet.Relation, + } + }) + } + + s = append(s, t.Object) + onSuccess.do(func() { + mt.Object = u[i*2+1] + }) + + res = append(res, &mt) + } + + if m.ReadOnly { + u, err = m.D.MappingManager().MapStringsToUUIDsReadOnly(ctx, s...) + } else { + u, err = m.D.MappingManager().MapStringsToUUIDs(ctx, s...) + } + if err != nil { + return nil, err + } + return res, nil +} + +func (m *Mapper) ToTuple(ctx context.Context, ts ...*RelationTuple) (res []*ketoapi.RelationTuple, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.ToTuple") + defer otelx.End(span, &err) + + onSuccess := newSuccess(&err) + defer onSuccess.apply() + + res = make([]*ketoapi.RelationTuple, 0, len(ts)) + u := make([]uuid.UUID, 0, len(ts)*2) + var s []string + + for _, t := range ts { + mt := ketoapi.RelationTuple{ + Namespace: t.Namespace, + Relation: t.Relation, + } + i := len(res) + + switch sub := t.Subject.(type) { + case *SubjectID: + u = append(u, sub.ID) + onSuccess.do(func() { + mt.SubjectID = new(s[2*i]) + }) + case *SubjectSet: + u = append(u, sub.Object) + onSuccess.do(func() { + mt.SubjectSet = &ketoapi.SubjectSet{ + Namespace: sub.Namespace, + Object: s[2*i], + Relation: sub.Relation, + } + }) + } + + u = append(u, t.Object) + onSuccess.do(func() { + mt.Object = s[2*i+1] + }) + + res = append(res, &mt) + } + + s, err = m.D.MappingManager().MapUUIDsToStrings(ctx, u...) + if err != nil { + return nil, err + } + return res, nil +} + +func (m *Mapper) FromSubjectSet(ctx context.Context, set *ketoapi.SubjectSet) (_ *SubjectSet, err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.FromSubjectSet") + defer otelx.End(span, &err) + + nm, err := m.D.Config(ctx).NamespaceManager() + if err != nil { + return nil, err + } + n, err := nm.GetNamespaceByName(ctx, set.Namespace) + if err != nil { + return nil, err + } + var u []uuid.UUID + if m.ReadOnly { + u, err = m.D.MappingManager().MapStringsToUUIDsReadOnly(ctx, set.Object) + } else { + u, err = m.D.MappingManager().MapStringsToUUIDs(ctx, set.Object) + } + if err != nil { + return nil, err + } + return &SubjectSet{ + Namespace: n.Name, + Object: u[0], + Relation: set.Relation, + }, nil +} + +func (m *Mapper) ToTree(ctx context.Context, tree *Tree) (_ *ketoapi.Tree[*ketoapi.RelationTuple], err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("keto/internal/relationtuple").Start(ctx, "Mapper.ToTree") + defer otelx.End(span, &err) + + var u []uuid.UUID + var targets []*string + + // collect walks the tree, and collects the UUIDs and target pointers + var collect func(*Tree) *ketoapi.Tree[*ketoapi.RelationTuple] + collect = func(tree *Tree) *ketoapi.Tree[*ketoapi.RelationTuple] { + res := &ketoapi.Tree[*ketoapi.RelationTuple]{ + Type: tree.Type, + Tuple: &ketoapi.RelationTuple{}, + Children: make([]*ketoapi.Tree[*ketoapi.RelationTuple], len(tree.Children)), + } + + switch sub := tree.Subject.(type) { + case *SubjectSet: + ss := &ketoapi.SubjectSet{ + Namespace: sub.Namespace, + Relation: sub.Relation, + } + res.Tuple.SubjectSet = ss + targets = append(targets, &ss.Object) + u = append(u, sub.Object) + case *SubjectID: + res.Tuple.SubjectID = new(string) + targets = append(targets, res.Tuple.SubjectID) + u = append(u, sub.ID) + } + + for i, c := range tree.Children { + res.Children[i] = collect(c) + } + + // to support backward compatibility: Nodes without children were always treated as Leaf, + // even if the type is Union or Intersection. Keep the backward compatibility on api level. + if len(res.Children) == 0 { + res.Type = ketoapi.TreeNodeLeaf + } + return res + } + + res := collect(tree) + + s, err := m.D.MappingManager().MapUUIDsToStrings(ctx, u...) + if err != nil { + return nil, err + } + + for i, ptr := range targets { + *ptr = s[i] + } + return res, nil +} diff --git a/internal/relationtuple/uuid_mapping_test.go b/internal/relationtuple/uuid_mapping_test.go new file mode 100644 index 0000000..571af7e --- /dev/null +++ b/internal/relationtuple/uuid_mapping_test.go @@ -0,0 +1,402 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package relationtuple_test + +import ( + "context" + "fmt" + "slices" + "testing" + + "github.com/ory/herodot" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +func runWithMappers(t *testing.T, p relationtuple.MapperProvider, runner func(t *testing.T, m *relationtuple.Mapper)) { + t.Run("mapper=readwrite", func(t *testing.T) { runner(t, p.Mapper()) }) + t.Run("mapper=readonly", func(t *testing.T) { runner(t, p.ReadOnlyMapper()) }) +} + +func TestMapper(t *testing.T) { + ctx := context.Background() + reg := driver.NewSqliteTestRegistry(t) + nspace := namespace.Namespace{ + Name: "test", + } + nspace2 := namespace.Namespace{ + Name: "test2", + } + require.NoError(t, reg.Config(ctx).Set(config.KeyNamespaces, []*namespace.Namespace{&nspace, &nspace2})) + + t.Run("items=relationships", func(t *testing.T) { + for _, tc := range []struct { + name string + rts []*ketoapi.RelationTuple + err error + }{ + { + name: "no relationships", + rts: []*ketoapi.RelationTuple{}, + }, + { + name: "one relation tuple", + rts: []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "object", + Relation: "relation", + SubjectID: new("subject"), + }, + }, + }, + { + name: "relation tuple without subject", + rts: []*ketoapi.RelationTuple{ + { + Namespace: nspace.Name, + Object: "object", + Relation: "relation", + }, + }, + err: ketoapi.ErrNilSubject(), + }, + { + name: "many relationships", + rts: func() []*ketoapi.RelationTuple { + rts := make([]*ketoapi.RelationTuple, 10) + for i := range rts { + rts[i] = &ketoapi.RelationTuple{ + Namespace: nspace.Name, + Object: fmt.Sprintf("object %d", i), + Relation: "relation", + SubjectID: new("subject"), + } + } + return rts + }(), + }, + { + name: "unknown namespace", + rts: []*ketoapi.RelationTuple{ + { + Namespace: "unknown", + Object: "object", + Relation: "relation", + SubjectID: new("subject"), + }, + }, + err: herodot.ErrNotFound(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) { + mapped, err := m.FromTuple(ctx, tc.rts...) + require.ErrorIs(t, err, tc.err) + if tc.err != nil { + // the rest only makes sense if we have no error + return + } + + assert.Len(t, mapped, len(tc.rts)) + actual, err := m.ToTuple(ctx, mapped...) + require.NoError(t, err) + assert.ElementsMatch(t, tc.rts, actual) + }) + }) + } + }) + + t.Run("item=relation query", func(t *testing.T) { + for _, tc := range []struct { + name string + query *ketoapi.RelationQuery + err error + }{ + { + name: "all fields nil", + query: &ketoapi.RelationQuery{}, + }, + { + name: "all fields set", + query: &ketoapi.RelationQuery{ + Namespace: new(nspace.Name), + Object: new("object"), + Relation: new("relation"), + SubjectID: new("subject"), + }, + }, + { + name: "subject set", + query: &ketoapi.RelationQuery{ + SubjectSet: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "object", + Relation: "relation", + }, + }, + }, + { + name: "non-mapped fields", + query: &ketoapi.RelationQuery{ + Relation: new("relation"), + SubjectSet: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Relation: "relation", + Object: "object", + }, + }, + }, + { + name: "unknown namespace", + query: &ketoapi.RelationQuery{ + Namespace: new("unknown"), + }, + err: herodot.ErrNotFound(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) { + mapped, err := m.FromQuery(ctx, tc.query) + require.ErrorIs(t, err, tc.err) + if tc.err != nil { + // the rest only makes sense if we have no error + return + } + + actual, err := m.ToQuery(ctx, mapped) + require.NoError(t, err) + assert.Equal(t, tc.query, actual) + }) + }) + } + }) + + t.Run("item=subject set", func(t *testing.T) { + for _, tc := range []struct { + name string + set *ketoapi.SubjectSet + err error + }{ + { + name: "basic subject set", + set: &ketoapi.SubjectSet{ + Namespace: nspace.Name, + Object: "object", + Relation: "relation", + }, + }, + { + name: "unknown namespace", + set: &ketoapi.SubjectSet{ + Namespace: "unknown", + Object: "object", + Relation: "relation", + }, + err: herodot.ErrNotFound(), + }, + } { + t.Run(tc.name, func(t *testing.T) { + runWithMappers(t, reg, func(t *testing.T, m *relationtuple.Mapper) { + mapped, err := reg.Mapper().FromSubjectSet(ctx, tc.set) + require.ErrorIs(t, err, tc.err) + if tc.err != nil { + // the rest only makes sense if we have no error + return + } + assert.NotZero(t, mapped.Object) + assert.Equal(t, nspace.Name, mapped.Namespace) + assert.Equal(t, tc.set.Relation, mapped.Relation) + }) + }) + } + }) + + t.Run("item=expand tree", func(t *testing.T) { + strs := make([]string, 3) + for i := range strs { + strs[i] = fmt.Sprintf("foo %d", i) + } + uuids, err := reg.MappingManager().MapStringsToUUIDs(ctx, strs...) + require.NoError(t, err) + + for _, tc := range []struct { + name string + tree *relationtuple.Tree + err error + }{ + { + name: "basic tree", + tree: &relationtuple.Tree{ + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuids[0]}, + }, + }, + { + name: "basic tree with children", + tree: &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: nspace.Name, + Object: uuids[0], + Relation: "members", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuids[1]}, + }, + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuids[2]}, + }, + }, + }, + }, + { + name: "deeply nested tree", + tree: &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: nspace.Name, + Object: uuids[0], + Relation: "members", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: nspace.Name, + Object: uuids[1], + Relation: "members", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuids[2]}, + }, + }, + }, + }, + }, + }, + { + name: "deeply nested tree with different namespaces", + tree: &relationtuple.Tree{ + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: nspace.Name, + Object: uuids[0], + Relation: "members", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeUnion, + Subject: &relationtuple.SubjectSet{ + Namespace: nspace2.Name, + Object: uuids[1], + Relation: "owners", + }, + Children: []*relationtuple.Tree{ + { + Type: ketoapi.TreeNodeLeaf, + Subject: &relationtuple.SubjectID{ID: uuids[2]}, + }, + }, + }, + }, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + mapped, err := reg.Mapper().ToTree(ctx, tc.tree) + require.ErrorIs(t, err, tc.err) + if tc.err != nil { + // the rest only makes sense if we have no error + return + } + + var checkTree func(*ketoapi.Tree[*ketoapi.RelationTuple], *relationtuple.Tree) + checkTree = func(mapped *ketoapi.Tree[*ketoapi.RelationTuple], original *relationtuple.Tree) { + switch s := original.Subject.(type) { + case *relationtuple.SubjectID: + require.NotNil(t, mapped.Tuple.SubjectID) + assert.Nil(t, mapped.Tuple.SubjectSet) + assert.Equal(t, strs[slices.Index(uuids, s.ID)], *mapped.Tuple.SubjectID) + case *relationtuple.SubjectSet: + require.NotNil(t, mapped.Tuple.SubjectSet) + assert.Nil(t, mapped.Tuple.SubjectID) + assert.Equal(t, s.Namespace, mapped.Tuple.SubjectSet.Namespace) + assert.Equal(t, strs[slices.Index(uuids, s.Object)], mapped.Tuple.SubjectSet.Object) + assert.Equal(t, s.Relation, mapped.Tuple.SubjectSet.Relation) + default: + t.Fatalf("expected subject to be set: %+v", mapped) + } + + assert.Equal(t, original.Type, mapped.Type) + require.Len(t, mapped.Children, len(original.Children)) + for i := range original.Children { + checkTree(mapped.Children[i], original.Children[i]) + } + } + checkTree(mapped, tc.tree) + }) + } + }) + + t.Run("suite=ro and rw mappers", func(t *testing.T) { + roMapper := reg.ReadOnlyMapper() + rwMapper := reg.Mapper() + + t.Run("case=ro manager does not insert into DB", func(t *testing.T) { + unmapped := &ketoapi.RelationQuery{Object: new("unmapped object")} + mapped, err := roMapper.FromQuery(ctx, unmapped) + require.NoError(t, err) + assert.NotNil(t, mapped.Object) + + reUnmapped, err := roMapper.ToQuery(ctx, mapped) + require.NoError(t, err) + assert.Empty(t, reUnmapped.Object) + }) + + t.Run("case=rw manager inserts into DB", func(t *testing.T) { + unmapped := &ketoapi.RelationQuery{Object: new("another unmapped object")} + mapped, err := rwMapper.FromQuery(ctx, unmapped) + require.NoError(t, err) + assert.NotNil(t, mapped.Object) + + reUnmapped, err := rwMapper.ToQuery(ctx, mapped) + require.NoError(t, err) + assert.NotEmpty(t, reUnmapped.Object) + }) + }) +} + +func BenchmarkReadOnlyMapper(b *testing.B) { + ctx := context.Background() + reg := driver.NewSqliteTestRegistry(b, driver.WithNamespaces([]*namespace.Namespace{{Name: "test"}})) + m := reg.ReadOnlyMapper() + + b.Run("FromTuple", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, err := m.FromTuple(ctx, &ketoapi.RelationTuple{ + Namespace: "test", + Object: "object", + Relation: "relation", + SubjectSet: &ketoapi.SubjectSet{ + Namespace: "test", + Object: "subject object", + Relation: "relation", + }, + }) + assert.NoError(b, err) + } + }) +} diff --git a/internal/testhelpers/driver.go b/internal/testhelpers/driver.go new file mode 100644 index 0000000..350305e --- /dev/null +++ b/internal/testhelpers/driver.go @@ -0,0 +1,30 @@ +package testhelpers + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/relationtuple" +) + +func CustomMapperNamespace(ctx context.Context) uuid.UUID { + return uuid.Nil +} + +func RegistryWithManagerWrapper(t *testing.T, reg driver.Registry, pageOpts ...keysetpagination.Option) (*Registry, *relationtuple.ManagerWrapper) { + mw := relationtuple.NewManagerWrapper(t, reg, pageOpts...) + return &Registry{mw: mw, Registry: reg}, mw +} + +type Registry struct { + mw *relationtuple.ManagerWrapper // managerProvider + driver.Registry +} + +func (d *Registry) RelationTupleManager() relationtuple.Manager { + return d.mw +} diff --git a/internal/testhelpers/scenarios.go b/internal/testhelpers/scenarios.go new file mode 100644 index 0000000..ccb1893 --- /dev/null +++ b/internal/testhelpers/scenarios.go @@ -0,0 +1,54 @@ +package testhelpers + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/driver/config" + "github.com/ory/keto/schema" +) + +type Scenario struct { + Name string + Strict bool + OrderedShardID bool + Opl string + InputTuples []string +} + +func (s Scenario) WithStrict(v bool) Scenario { + s.Strict = v + return s +} + +func (s Scenario) WithOrderedShardID() Scenario { + s.OrderedShardID = true + return s +} + +func (s Scenario) Run(t *testing.T, f func(t *testing.T, reg driver.Registry)) { + name := s.Name + if s.Strict { + name += " (strict)" + } + t.Run(name, func(t *testing.T) { + if s.Opl != "" { + _, errs := schema.Parse(s.Opl) + require.Len(t, errs, 0) + } + opts := []driver.TestRegistryOption{ + driver.WithOPL(s.Opl), + driver.WithMapperNamespace(CustomMapperNamespace), + driver.WithConfig(config.KeyFeatureFlagStrictMode, s.Strict), + } + if s.OrderedShardID { + opts = append(opts, driver.WithOrderedShardID()) + } + reg := driver.NewSqliteTestRegistry(t, opts...) + + MapAndInsertTuplesFromString(t, reg, s.InputTuples) + f(t, reg) + }) +} diff --git a/internal/testhelpers/tuples.go b/internal/testhelpers/tuples.go new file mode 100644 index 0000000..b623183 --- /dev/null +++ b/internal/testhelpers/tuples.go @@ -0,0 +1,136 @@ +package testhelpers + +import ( + "testing" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/relationtuple" + "github.com/ory/keto/ketoapi" +) + +// TupleFactory parses human-readable tuples like "File:x#view@User:u" into +// internal relation tuples. It adds a randomized suffix to namespaces and maps +// object/subject IDs to UUIDs unique per factory. +type TupleFactory struct { + suffix string + seed uuid.UUID +} + +func NewTupleFactory() *TupleFactory { + seed := uuid.Must(uuid.NewV4()) + return &TupleFactory{suffix: "_" + seed.String(), seed: seed} +} + +// NS returns the namespace name with the factory's isolation suffix applied. +func (f *TupleFactory) NS(namespace string) string { return namespace + f.suffix } + +// UUID maps an object or subject ID to a UUID unique to this factory. +func (f *TupleFactory) UUID(id string) uuid.UUID { return uuid.NewV5(f.seed, id) } + +func (f *TupleFactory) Tuple(t testing.TB, s string) *relationtuple.RelationTuple { + t.Helper() + rt := APITupleFromString(t, s) + result := &relationtuple.RelationTuple{ + Namespace: f.NS(rt.Namespace), + Object: f.UUID(rt.Object), + Relation: rt.Relation, + } + switch { + case rt.SubjectID != nil: + result.Subject = &relationtuple.SubjectID{ID: f.UUID(*rt.SubjectID)} + case rt.SubjectSet != nil: + result.Subject = &relationtuple.SubjectSet{ + Namespace: f.NS(rt.SubjectSet.Namespace), + Object: f.UUID(rt.SubjectSet.Object), + Relation: rt.SubjectSet.Relation, + } + default: + t.Fatalf("tuple %q has no subject", s) + } + return result +} + +func APITupleFromString(t testing.TB, s string) *ketoapi.RelationTuple { + t.Helper() + rt, err := (&ketoapi.RelationTuple{}).FromString(s) + require.NoError(t, err) + return rt +} + +func TupleFromString(t testing.TB, s string) *relationtuple.RelationTuple { + t.Helper() + rt := APITupleFromString(t, s) + result := &relationtuple.RelationTuple{ + Namespace: rt.Namespace, + Object: toUUID(rt.Object), + Relation: rt.Relation, + } + switch { + case rt.SubjectID != nil: + result.Subject = &relationtuple.SubjectID{ID: toUUID(*rt.SubjectID)} + case rt.SubjectSet != nil: + result.Subject = &relationtuple.SubjectSet{ + Namespace: rt.SubjectSet.Namespace, + Object: toUUID(rt.SubjectSet.Object), + Relation: rt.SubjectSet.Relation, + } + default: + t.Fatal("invalid tuple") + } + return result +} + +func SubjectFromString(t testing.TB, s string) relationtuple.Subject { + t.Helper() + rt, err := (&ketoapi.RelationTuple{}).FromString(" :object#relation@" + s) + require.NoError(t, err) + + switch { + case rt.SubjectID != nil: + return &relationtuple.SubjectID{ID: toUUID(*rt.SubjectID)} + case rt.SubjectSet != nil: + return &relationtuple.SubjectSet{ + Namespace: rt.SubjectSet.Namespace, + Object: toUUID(rt.SubjectSet.Object), + Relation: rt.SubjectSet.Relation, + } + default: + t.Fatal("invalid subject") + return nil + } +} + +func SubjectSetFromString(t testing.TB, s string) *relationtuple.SubjectSet { + t.Helper() + subject := SubjectFromString(t, s) + subjectSet, ok := subject.(*relationtuple.SubjectSet) + require.True(t, ok, "expected subject to be a SubjectSet") + return subjectSet +} + +type deps interface { + relationtuple.MapperProvider + relationtuple.ManagerProvider +} + +func MapAndInsertTuplesFromString(t testing.TB, d deps, tuples []string) { + t.Helper() + relationTuples := make([]*relationtuple.RelationTuple, len(tuples)) + for i, tuple := range tuples { + relationTuples[i] = TupleFromString(t, tuple) + } + require.NoError(t, d.RelationTupleManager().WriteRelationTuples(t.Context(), relationTuples...)) +} + +func MapAndInsertTuples(t testing.TB, d deps, tuples ...*ketoapi.RelationTuple) { + t.Helper() + its, err := d.Mapper().FromTuple(t.Context(), tuples...) + require.NoError(t, err) + require.NoError(t, d.RelationTupleManager().WriteRelationTuples(t.Context(), its...)) +} + +func toUUID(s string) uuid.UUID { + return uuid.NewV5(uuid.Nil, s) +} diff --git a/internal/x/dbx/dsn_cockroach.go b/internal/x/dbx/dsn_cockroach.go new file mode 100644 index 0000000..b4ed276 --- /dev/null +++ b/internal/x/dbx/dsn_cockroach.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !nocrdb && !nocockroach + +package dbx + +import ( + "testing" + + "github.com/ory/x/sqlcon/dockertest" +) + +func RunCockroach(t testing.TB, testDB string) (string, error) { + url := dockertest.RunTestCockroachDBWithVersion(t, "latest-v25.4") + if err := createDB(t, url, testDB); err != nil { + return "", err + } + return withDbName(url, testDB), nil +} diff --git a/internal/x/dbx/dsn_mysql.go b/internal/x/dbx/dsn_mysql.go new file mode 100644 index 0000000..481a1dc --- /dev/null +++ b/internal/x/dbx/dsn_mysql.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !nomysql + +package dbx + +import ( + "testing" + "time" + + "github.com/ory/x/sqlcon/dockertest" +) + +func RunMySQL(t testing.TB, testDB string) (string, error) { + url := dockertest.RunTestMySQLWithVersion(t, "8.0") + time.Sleep(1 * time.Second) + if err := createDB(t, url, testDB); err != nil { + return "", err + } + return withDbName(url, testDB), nil +} diff --git a/internal/x/dbx/dsn_nocockroach.go b/internal/x/dbx/dsn_nocockroach.go new file mode 100644 index 0000000..a93c0b7 --- /dev/null +++ b/internal/x/dbx/dsn_nocockroach.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build nocrdb || nocockroach + +package dbx + +import ( + "testing" +) + +func RunCockroach(testing.TB, string) (string, error) { return "", nil } diff --git a/internal/x/dbx/dsn_nomysql.go b/internal/x/dbx/dsn_nomysql.go new file mode 100644 index 0000000..aeac03b --- /dev/null +++ b/internal/x/dbx/dsn_nomysql.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build nomysql + +package dbx + +import ( + "testing" +) + +func RunMySQL(testing.TB, string) (string, error) { return "", nil } diff --git a/internal/x/dbx/dsn_nopostgres.go b/internal/x/dbx/dsn_nopostgres.go new file mode 100644 index 0000000..e9a4156 --- /dev/null +++ b/internal/x/dbx/dsn_nopostgres.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build nopostgres + +package dbx + +import ( + "testing" +) + +func RunPostgres(testing.TB, string) (string, error) { return "", nil } diff --git a/internal/x/dbx/dsn_postgres.go b/internal/x/dbx/dsn_postgres.go new file mode 100644 index 0000000..2ad175a --- /dev/null +++ b/internal/x/dbx/dsn_postgres.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !nopostgres + +package dbx + +import ( + "testing" + + "github.com/ory/x/sqlcon/dockertest" +) + +func RunPostgres(t testing.TB, testDB string) (string, error) { + url := dockertest.RunTestPostgreSQLWithVersion(t, "16") + if err := createDB(t, url, testDB); err != nil { + return "", err + } + return withDbName(url, testDB), nil +} diff --git a/internal/x/dbx/dsn_sqlite.go b/internal/x/dbx/dsn_sqlite.go new file mode 100644 index 0000000..17d47d5 --- /dev/null +++ b/internal/x/dbx/dsn_sqlite.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbx + +import ( + "testing" + + "github.com/ory/x/dbal" +) + +func GetSqlite(t testing.TB) *DsnT { + return &DsnT{ + MigrateUp: true, + MigrateDown: false, + Conn: dbal.NewSQLiteTestDatabase(t), + Name: "sqlite", + } +} diff --git a/internal/x/dbx/dsn_testutils.go b/internal/x/dbx/dsn_testutils.go new file mode 100644 index 0000000..625cc75 --- /dev/null +++ b/internal/x/dbx/dsn_testutils.go @@ -0,0 +1,188 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbx + +import ( + "crypto/rand" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/go-sql-driver/mysql" + "github.com/stretchr/testify/require" + "github.com/tidwall/sjson" + "golang.org/x/sync/errgroup" + + "github.com/ory/pop/v6" +) + +type DsnT struct { + Name string + Conn string + MigrateUp, MigrateDown bool +} + +const mySQLSchema = "mysql://" + +func mySQLWithDbName(dsn string, db string) string { + cfg, err := mysql.ParseDSN(strings.TrimPrefix(dsn, mySQLSchema)) + if err != nil { + return "" + } + cfg.DBName = db + return mySQLSchema + cfg.FormatDSN() +} + +func withDbName(dsn string, db string) string { + // Special case for mysql, because their URLs are not parsable. + if strings.HasPrefix(dsn, mySQLSchema) { + return mySQLWithDbName(dsn, db) + } + + u, err := url.Parse(dsn) + if err != nil { + return "" + } + u.Path = db + + return u.String() +} + +func dbName() string { + var buf [20]byte + _, _ = rand.Read(buf[:]) + return fmt.Sprintf("testdb_%x", buf) +} + +func openAndPing(url string) (*pop.Connection, error) { + conn, err := pop.NewConnection(&pop.ConnectionDetails{URL: url}) + if err != nil { + return nil, fmt.Errorf("failed to connect to %q: %w", url, err) + } + for range 120 { + fmt.Println("trying to open connection to", url) + if err := conn.Open(); err != nil { + // return nil, fmt.Errorf("failed to open connection: %w", err) + time.Sleep(1 * time.Second) + continue + } + if err := Ping(conn); err == nil { + break + } + time.Sleep(1 * time.Second) + } + if err := Ping(conn); err != nil { + _ = conn.Close() + return nil, fmt.Errorf("failed to ping: %w", err) + } + return conn, nil +} + +func createDB(t testing.TB, url string, dbName string) (err error) { + var conn *pop.Connection + + if conn, err = openAndPing(url); err != nil { + return fmt.Errorf("failed to connect to %q: %w", url, err) + } + t.Cleanup(func() { + if err := conn.Close(); err != nil { + t.Logf("could not close connection for %q in %q: %v", dbName, url, err) + } + }) + + if err := conn.RawQuery("CREATE DATABASE " + dbName).Exec(); err != nil { + return fmt.Errorf("failed to create db %q in %q: %w", dbName, url, err) + } + + t.Cleanup(func() { + if err := conn.RawQuery("DROP DATABASE " + dbName).Exec(); err != nil { + t.Logf("could not drop database %q in %q: %v", dbName, url, err) + } + }) + + return + +} + +func GetDSNs(t testing.TB) []*DsnT { + dsns := []*DsnT{GetSqlite(t)} + + if !testing.Short() { + var mysql, postgres, cockroach string + testDB := dbName() + + var eg errgroup.Group + eg.Go(func() (err error) { + postgres, err = RunPostgres(t, testDB) + return + }) + eg.Go(func() (err error) { + mysql, err = RunMySQL(t, testDB) + return + }) + eg.Go(func() (err error) { + cockroach, err = RunCockroach(t, testDB) + return + }) + require.NoError(t, eg.Wait()) + + if mysql != "" { + dsns = append(dsns, &DsnT{ + Name: "mysql", + Conn: mysql, + MigrateUp: true, + MigrateDown: true, + }) + } + if postgres != "" { + dsns = append(dsns, &DsnT{ + Name: "postgres", + Conn: postgres, + MigrateUp: true, + MigrateDown: true, + }) + } + if cockroach != "" { + dsns = append(dsns, &DsnT{ + Name: "cockroach", + Conn: cockroach, + MigrateUp: true, + MigrateDown: true, + }) + } + } + + require.NotZero(t, len(dsns), "expected to run against at least one database") + return dsns +} + +func ConfigFile(t testing.TB, values map[string]interface{}) string { + dir := t.TempDir() + fn := filepath.Join(dir, "keto.json") + + c := []byte("{}") + for key, val := range values { + var err error + c, err = sjson.SetBytes(c, key, val) + require.NoError(t, err) + } + + require.NoError(t, os.WriteFile(fn, c, 0600)) + + return fn +} + +type pinger interface{ Ping() error } + +func Ping(conn *pop.Connection) error { + p, ok := conn.Store.(pinger) + if !ok { + return fmt.Errorf("connection not opened: Store is %T", conn.Store) + } + return p.Ping() +} diff --git a/internal/x/dbx/dsn_testutils_test.go b/internal/x/dbx/dsn_testutils_test.go new file mode 100644 index 0000000..fa731f1 --- /dev/null +++ b/internal/x/dbx/dsn_testutils_test.go @@ -0,0 +1,64 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbx + +import ( + "testing" + + "github.com/ory/pop/v6" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//nolint:gosec // Test DSNs with dummy credentials, not real secrets. +func Test_withDbName(t *testing.T) { + type args struct { + dsn string + db string + } + tests := []struct { + name string + args args + want string + }{{ + name: "postgres", + args: args{ + dsn: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", + db: "mydb", + }, + want: "postgres://postgres:postgres@localhost:5432/mydb?sslmode=disable", + }, { + name: "cockroach", + args: args{ + dsn: "cockroach://root@localhost:49364/defaultdb?sslmode=disable", + db: "foo", + }, + want: "cockroach://root@localhost:49364/foo?sslmode=disable", + }, { + name: "mysql", + args: args{ + dsn: "mysql://root:secret@(localhost:49394)/mysql?parseTime=true&multiStatements=true", + db: "testdb", + }, + want: "mysql://root:secret@tcp(localhost:49394)/testdb?multiStatements=true&parseTime=true", + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, withDbName(tt.args.dsn, tt.args.db)) + }) + } +} + +func Test_GetDSNs_can_connect_to_each_db(t *testing.T) { + for _, db := range GetDSNs(t) { + t.Run("dsn="+db.Name, func(t *testing.T) { + t.Parallel() + conn, err := pop.NewConnection(&pop.ConnectionDetails{URL: db.Conn}) + require.NoError(t, err) + assert.NoError(t, conn.Open()) + assert.NoError(t, Ping(conn)) + assert.NoError(t, conn.Close()) + }) + } +} diff --git a/internal/x/graph/graph_utils.go b/internal/x/graph/graph_utils.go new file mode 100644 index 0000000..108e1e8 --- /dev/null +++ b/internal/x/graph/graph_utils.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package graph + +import ( + "context" + "fmt" + "sync" + + "github.com/ory/keto/internal/relationtuple" +) + +type contextKey string + +const visitedMapKey = contextKey("visitedMap") + +type stringSet struct { + m map[string]struct{} + l sync.Mutex +} + +func newStringSet() *stringSet { + return &stringSet{m: make(map[string]struct{})} +} + +func (s *stringSet) addNoDuplicate(el fmt.Stringer) bool { + s.l.Lock() + defer s.l.Unlock() + + if _, found := s.m[el.String()]; found { + return true + } + s.m[el.String()] = struct{}{} + return false +} + +func InitVisited(ctx context.Context) context.Context { + if _, ok := ctx.Value(visitedMapKey).(*stringSet); !ok { + ctx = context.WithValue(ctx, visitedMapKey, newStringSet()) + } + return ctx +} + +func CheckAndAddVisited(ctx context.Context, current relationtuple.Subject) (context.Context, bool) { + set, ok := ctx.Value(visitedMapKey).(*stringSet) + if !ok { + set = newStringSet() + ctx = context.WithValue(ctx, visitedMapKey, set) + } + + return ctx, set.addNoDuplicate(current.UniqueID()) +} diff --git a/internal/x/graph/graph_utils_test.go b/internal/x/graph/graph_utils_test.go new file mode 100644 index 0000000..754c25e --- /dev/null +++ b/internal/x/graph/graph_utils_test.go @@ -0,0 +1,127 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package graph + +import ( + "context" + "sync" + "testing" + + "github.com/gofrs/uuid" + + "github.com/ory/keto/internal/relationtuple" + + "github.com/stretchr/testify/assert" +) + +func TestEngineUtilsProvider_CheckVisited(t *testing.T) { + a, b, c, d, e := uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()) + t.Run("case=finds cycle", func(t *testing.T) { + linkedList := []relationtuple.SubjectSet{{ + Namespace: "1", + Object: a, + Relation: "connected", + }, { + Namespace: "1", + Object: b, + Relation: "connected", + }, { + Namespace: "1", + Object: c, + Relation: "connected", + }, { + Namespace: "1", + Object: b, + Relation: "connected", + }, { + Namespace: "1", + Object: d, + Relation: "connected", + }} + + ctx := context.Background() + var isThereACycle bool + for i := range linkedList { + ctx, isThereACycle = CheckAndAddVisited(ctx, &linkedList[i]) + if isThereACycle { + break + } + } + + assert.Equal(t, isThereACycle, true) + }) + + t.Run("case=ignores if no cycle", func(t *testing.T) { + list := []relationtuple.SubjectSet{{ + Namespace: "1", + Object: a, + Relation: "connected", + }, { + Namespace: "1", + Object: b, + Relation: "connected", + }, { + Namespace: "1", + Object: c, + Relation: "connected", + }, { + Namespace: "1", + Object: d, + Relation: "connected", + }, { + Namespace: "1", + Object: e, + Relation: "connected", + }} + + ctx := context.Background() + var isThereACycle bool + for i := range list { + ctx, isThereACycle = CheckAndAddVisited(ctx, &list[i]) + if isThereACycle { + break + } + } + + assert.Equal(t, isThereACycle, false) + }) + + t.Run("case=no race condition during adding", func(t *testing.T) { + racyObj := uuid.Must(uuid.NewV4()) + otherObj := uuid.Must(uuid.NewV4()) + // we repeat this test a few times to ensure we don't have a race condition + // the race detector alone was not able to catch it + for i := 0; i < 500; i++ { + subject := &relationtuple.SubjectSet{ + Namespace: "default", + Object: racyObj, + Relation: "connected", + } + + ctx, _ := CheckAndAddVisited( + context.Background(), + &relationtuple.SubjectSet{Object: otherObj}, + ) + var wg sync.WaitGroup + var aCycle, bCycle bool + var aCtx, bCtx context.Context + + wg.Add(2) + go func() { + aCtx, aCycle = CheckAndAddVisited(ctx, subject) + wg.Done() + }() + go func() { + bCtx, bCycle = CheckAndAddVisited(ctx, subject) + wg.Done() + }() + + wg.Wait() + // one should be true, and one false + assert.False(t, aCycle && bCycle) + assert.True(t, aCycle || bCycle) + assert.Equal(t, aCtx, bCtx) + } + }) +} diff --git a/internal/x/helpers.go b/internal/x/helpers.go new file mode 100644 index 0000000..114e1bf --- /dev/null +++ b/internal/x/helpers.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "github.com/gofrs/uuid" +) + +func UUIDs(n int) []uuid.UUID { + res := make([]uuid.UUID, n) + for i := range res { + res[i] = uuid.Must(uuid.NewV4()) + } + return res +} diff --git a/internal/x/max_depth.go b/internal/x/max_depth.go new file mode 100644 index 0000000..ad60c6f --- /dev/null +++ b/internal/x/max_depth.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "net/url" + "strconv" + + "github.com/ory/herodot" +) + +func GetMaxDepthFromQuery(q url.Values) (int, error) { + if !q.Has("max-depth") { + return 0, nil + } + + maxDepth, err := strconv.ParseInt(q.Get("max-depth"), 0, 0) + if err != nil { + return 0, herodot.ErrBadRequest().WithErrorf("unable to parse 'max-depth' query parameter to int: %s", err) + } + + return int(maxDepth), err +} diff --git a/internal/x/registry.go b/internal/x/registry.go new file mode 100644 index 0000000..0b0bb59 --- /dev/null +++ b/internal/x/registry.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x + +import ( + "context" + + "connectrpc.com/connect" + "github.com/gofrs/uuid" +) + +type NetworkIDProvider interface { + NetworkID(context.Context) uuid.UUID +} + +type HandlerOptionsProvider interface { + HandlerOptions() []connect.HandlerOption +} diff --git a/internal/x/validate/validate.go b/internal/x/validate/validate.go new file mode 100644 index 0000000..4e72987 --- /dev/null +++ b/internal/x/validate/validate.go @@ -0,0 +1,75 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package validate + +import ( + "fmt" + "io" + "net/http" + "strings" + + "github.com/ory/herodot" +) + +type Validator func(r *http.Request) (ok bool, reason string) + +// All runs all validators and returns an error if any of them fail. It returns +// a ErrBadRequest with all failed validation messages. +func All(r *http.Request, validator ...Validator) error { + var reasons []string + for _, v := range validator { + if ok, msg := v(r); !ok { + reasons = append(reasons, msg) + } + } + if len(reasons) > 0 { + return herodot.ErrBadRequest().WithReason(strings.Join(reasons, "; ")) + } + return nil +} + +// NoExtraQueryParams returns a validator that checks if the request has any +// query parameters that are not in the except list. +func NoExtraQueryParams(except ...string) Validator { + return func(req *http.Request) (ok bool, reason string) { + allowed := make(map[string]struct{}, len(except)) + for _, e := range except { + allowed[e] = struct{}{} + } + for key := range req.URL.Query() { + if _, found := allowed[key]; !found { + return false, fmt.Sprintf("query parameter key %q unknown", key) + } + } + return true, "" + } +} + +// QueryParamsContainsOneOf returns a validator that checks if the request has +// at least one of the specified query parameters. +func QueryParamsContainsOneOf(keys ...string) Validator { + return func(req *http.Request) (ok bool, reason string) { + oneOfKeys := make(map[string]struct{}, len(keys)) + for _, k := range keys { + oneOfKeys[k] = struct{}{} + } + for key := range req.URL.Query() { + if _, found := oneOfKeys[key]; found { + return true, "" + } + } + return false, fmt.Sprintf("query parameters must specify at least one of the following: %s", strings.Join(keys, ", ")) + } +} + +// HasEmptyBody returns a validator that checks if the request body is empty. +func HasEmptyBody() Validator { + return func(r *http.Request) (ok bool, reason string) { + _, err := r.Body.Read([]byte{}) + if err != io.EOF { + return false, "body is not empty" + } + return true, "" + } +} diff --git a/internal/x/validate/validate_test.go b/internal/x/validate/validate_test.go new file mode 100644 index 0000000..50ff5d8 --- /dev/null +++ b/internal/x/validate/validate_test.go @@ -0,0 +1,116 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package validate_test + +import ( + "io" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/keto/internal/x/validate" +) + +func toURL(t *testing.T, s string) *url.URL { + u, err := url.Parse(s) + require.NoError(t, err) + return u +} + +func TestValidateNoExtraParams(t *testing.T) { + for _, tt := range []struct { + name string + req *http.Request + assertErr assert.ErrorAssertionFunc + }{ + { + name: "empty", + req: &http.Request{URL: toURL(t, "https://example.com")}, + assertErr: assert.NoError, + }, + { + name: "all params", + req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=baz")}, + assertErr: assert.NoError, + }, + { + name: "extra params", + req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=2&baz=3")}, + assertErr: assert.Error, + }, + } { + t.Run("case="+tt.name, func(t *testing.T) { + err := validate.All(tt.req, validate.NoExtraQueryParams("foo", "bar")) + tt.assertErr(t, err) + }) + } +} + +func TestQueryParamsContainsOneOf(t *testing.T) { + for _, tt := range []struct { + name string + req *http.Request + assertErr assert.ErrorAssertionFunc + }{ + { + name: "empty", + req: &http.Request{URL: toURL(t, "https://example.com")}, + assertErr: assert.Error, + }, + { + name: "other", + req: &http.Request{URL: toURL(t, "https://example.com?a=1&b=2&c=3")}, + assertErr: assert.Error, + }, + { + name: "one", + req: &http.Request{URL: toURL(t, "https://example.com?foo=1")}, + assertErr: assert.NoError, + }, + { + name: "all params", + req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=baz")}, + assertErr: assert.NoError, + }, + { + name: "extra params", + req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=2&baz=3")}, + assertErr: assert.NoError, + }, + } { + t.Run("case="+tt.name, func(t *testing.T) { + err := validate.All(tt.req, validate.QueryParamsContainsOneOf("foo", "bar")) + tt.assertErr(t, err) + }) + } +} + +func TestValidateHasEmptyBody(t *testing.T) { + for _, tt := range []struct { + name string + req *http.Request + assertErr assert.ErrorAssertionFunc + }{ + { + name: "empty body", + req: &http.Request{Body: io.NopCloser(strings.NewReader(""))}, + assertErr: assert.NoError, + }, + { + name: "non-empty body", + req: &http.Request{Body: io.NopCloser(strings.NewReader("content"))}, + assertErr: assert.Error, + }, + } { + t.Run("case="+tt.name, func(t *testing.T) { + err := validate.All(tt.req, validate.HasEmptyBody()) + tt.assertErr(t, err) + }) + } + +} diff --git a/ketoapi/cmd_output.go b/ketoapi/cmd_output.go new file mode 100644 index 0000000..efba290 --- /dev/null +++ b/ketoapi/cmd_output.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +func (r *RelationTuple) Header() []string { + return []string{ + "NAMESPACE", + "OBJECT ID", + "RELATION NAME", + "SUBJECT", + } +} + +func (r *RelationTuple) Columns() []string { + sub := "" + switch { + case r.SubjectID != nil: + sub = *r.SubjectID + case r.SubjectSet != nil: + sub = r.SubjectSet.String() + } + + return []string{ + r.Namespace, + r.Object, + r.Relation, + sub, + } +} + +func (r *RelationTuple) Interface() interface{} { + return r +} diff --git a/ketoapi/enc_proto.go b/ketoapi/enc_proto.go new file mode 100644 index 0000000..ec596bb --- /dev/null +++ b/ketoapi/enc_proto.go @@ -0,0 +1,189 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +import ( + "github.com/pkg/errors" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +type ( + TupleData interface { + GetSubject() *rts.Subject + GetObject() string + GetNamespace() string + GetRelation() string + } + queryData interface { + GetSubject() *rts.Subject + GetObject() *string + GetNamespace() *string + GetRelation() *string + } +) + +func (r *RelationTuple) FromDataProvider(d TupleData) (*RelationTuple, error) { + switch s := d.GetSubject().GetRef().(type) { + case nil: + return nil, errors.WithStack(ErrNilSubject()) + case *rts.Subject_Set: + r.SubjectSet = &SubjectSet{ + Namespace: s.Set.Namespace, + Object: s.Set.Object, + Relation: s.Set.Relation, + } + case *rts.Subject_Id: + r.SubjectID = new(s.Id) + } + + r.Object = d.GetObject() + r.Namespace = d.GetNamespace() + r.Relation = d.GetRelation() + + return r, nil +} + +func (r *RelationTuple) ToProto() *rts.RelationTuple { + res := &rts.RelationTuple{ + Namespace: r.Namespace, + Object: r.Object, + Relation: r.Relation, + } + if r.SubjectID != nil { + res.Subject = rts.NewSubjectID(*r.SubjectID) + } else { + res.Subject = rts.NewSubjectSet(r.SubjectSet.Namespace, r.SubjectSet.Object, r.SubjectSet.Relation) + } + return res +} + +func (r *RelationTuple) FromProto(proto *rts.RelationTuple) *RelationTuple { + r = &RelationTuple{ + Namespace: proto.Namespace, + Object: proto.Object, + Relation: proto.Relation, + } + switch subject := proto.Subject.Ref.(type) { + case *rts.Subject_Id: + r.SubjectID = new(subject.Id) + case *rts.Subject_Set: + r.SubjectSet = &SubjectSet{ + Namespace: subject.Set.Namespace, + Object: subject.Set.Object, + Relation: subject.Set.Relation, + } + } + + return r +} + +func (q *RelationQuery) FromDataProvider(d queryData) *RelationQuery { + q.Namespace = d.GetNamespace() + q.Object = d.GetObject() + q.Relation = d.GetRelation() + q.SubjectID = nil + q.SubjectSet = nil + + if s := d.GetSubject(); s != nil { + switch sub := s.Ref.(type) { + case *rts.Subject_Id: + q.SubjectID = new(sub.Id) + case *rts.Subject_Set: + q.SubjectSet = &SubjectSet{ + Namespace: sub.Set.Namespace, + Object: sub.Set.Object, + Relation: sub.Set.Relation, + } + } + } + return q +} + +func (q *RelationQuery) ToProto() *rts.RelationQuery { + res := &rts.RelationQuery{ + Namespace: q.Namespace, + Object: q.Object, + Relation: q.Relation, + } + if q.SubjectID != nil { + res.Subject = rts.NewSubjectID(*q.SubjectID) + } else if q.SubjectSet != nil { + res.Subject = rts.NewSubjectSet(q.SubjectSet.Namespace, q.SubjectSet.Object, q.SubjectSet.Relation) + } + return res +} + +func (t *Tree[NodeT]) ToProto() *rts.SubjectTree { + res := &rts.SubjectTree{ + NodeType: t.Type.ToProto(), + Children: make([]*rts.SubjectTree, len(t.Children)), + } + res.Tuple = t.Tuple.ToProto() + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + res.Subject = res.Tuple.Subject + for i := range t.Children { + res.Children[i] = t.Children[i].ToProto() + } + return res +} + +func TreeFromProto[T tuple[T]](pt *rts.SubjectTree) *Tree[T] { + t := new(Tree[T]) + t.Type = TreeNodeType("").FromProto(pt.NodeType) + + var tuple T + if pt.Tuple == nil { + // legacy case: fetch from deprecated fields + //lint:ignore SA1019 backwards compatibility + //nolint:staticcheck + switch sub := pt.Subject.Ref.(type) { + case *rts.Subject_Id: + pt.Tuple.Subject = rts.NewSubjectID(sub.Id) + case *rts.Subject_Set: + pt.Tuple.Subject = rts.NewSubjectSet( + sub.Set.Namespace, + sub.Set.Object, + sub.Set.Relation, + ) + } + } + t.Tuple = tuple.FromProto(pt.Tuple) + + t.Children = make([]*Tree[T], len(pt.Children)) + for i := range pt.Children { + t.Children[i] = TreeFromProto[T](pt.Children[i]) + } + + return t +} + +func (t TreeNodeType) ToProto() rts.NodeType { + switch t { + case TreeNodeLeaf: + return rts.NodeType_NODE_TYPE_LEAF + case TreeNodeUnion: + return rts.NodeType_NODE_TYPE_UNION + case TreeNodeExclusion: + return rts.NodeType_NODE_TYPE_EXCLUSION + case TreeNodeIntersection: + return rts.NodeType_NODE_TYPE_INTERSECTION + } + return rts.NodeType_NODE_TYPE_UNSPECIFIED +} + +func (TreeNodeType) FromProto(pt rts.NodeType) TreeNodeType { + switch pt { + case rts.NodeType_NODE_TYPE_LEAF: + return TreeNodeLeaf + case rts.NodeType_NODE_TYPE_UNION: + return TreeNodeUnion + case rts.NodeType_NODE_TYPE_EXCLUSION: + return TreeNodeExclusion + case rts.NodeType_NODE_TYPE_INTERSECTION: + return TreeNodeIntersection + } + return TreeNodeUnspecified +} diff --git a/ketoapi/enc_string.go b/ketoapi/enc_string.go new file mode 100644 index 0000000..4b297da --- /dev/null +++ b/ketoapi/enc_string.go @@ -0,0 +1,153 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +import ( + "fmt" + "strings" + + "github.com/ory/herodot" + "github.com/pkg/errors" +) + +func ErrMalformedInput() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("malformed string input") +} + +func (r *RelationTuple) String() string { + if r == nil { + return "" + } + sb := strings.Builder{} + sb.WriteString(r.Namespace) + sb.WriteRune(':') + sb.WriteString(r.Object) + sb.WriteRune('#') + sb.WriteString(r.Relation) + sb.WriteRune('@') + + if r.SubjectID != nil { + sb.WriteString(*r.SubjectID) + } else if r.SubjectSet != nil { + sb.WriteString(r.SubjectSet.String()) + } else { + sb.WriteString("") + } + return sb.String() +} + +func (r *RelationTuple) FromString(s string) (*RelationTuple, error) { + var ( + objectAndRelationAndSubject string + relationAndSubject string + subject string + ok bool + ) + if r.Namespace, objectAndRelationAndSubject, ok = strings.Cut(s, ":"); !ok { + return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain ':'")) + } + + if r.Object, relationAndSubject, ok = strings.Cut(objectAndRelationAndSubject, "#"); !ok { + return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain '#'")) + } + + if r.Relation, subject, ok = strings.Cut(relationAndSubject, "@"); !ok { + return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected input to contain '@'")) + } + + // remove optional brackets around the subject set + subject = strings.Trim(subject, "()") + if strings.Contains(subject, ":") { + subSet, err := (&SubjectSet{}).FromString(subject) + if err != nil { + return nil, err + } + r.SubjectSet = subSet + } else { + r.SubjectID = &subject + } + + return r, nil +} + +func (s *SubjectSet) String() string { + if s.Relation == "" { + return fmt.Sprintf("%s:%s", s.Namespace, s.Object) + } + return fmt.Sprintf("%s:%s#%s", s.Namespace, s.Object, s.Relation) +} + +func (s *SubjectSet) FromString(str string) (*SubjectSet, error) { + // If there is no '#' we have a subject set without a relation, such as + // Users:Bob, which just means that the relation is empty. + namespaceAndObject, relation, _ := strings.Cut(str, "#") + + namespace, object, ok := strings.Cut(namespaceAndObject, ":") + if !ok { + return nil, errors.WithStack(ErrMalformedInput().WithDebug("expected subject set to contain ':'")) + } + + return &SubjectSet{ + Namespace: namespace, + Object: object, + Relation: relation, + }, nil +} + +func (t TreeNodeType) String() string { + return string(t) +} + +func (t *Tree[NodeT]) Label() string { + if t == nil { + return "" + } + + return t.Tuple.String() +} + +func (t *Tree[NodeT]) String() string { + if t == nil { + return "" + } + + nodeLabel := t.Label() + + if t.Type == TreeNodeLeaf { + return fmt.Sprintf("∋ %s️", nodeLabel) + } + + children := make([]string, len(t.Children)) + for i, c := range t.Children { + var indent string + if i == len(t.Children)-1 { + indent = " " + } else { + indent = "│ " + } + children[i] = strings.Join(strings.Split(c.String(), "\n"), "\n"+indent) + } + + setOperation := "" + switch t.Type { + case TreeNodeIntersection: + setOperation = "and" + case TreeNodeUnion: + setOperation = "or" + case TreeNodeExclusion: + setOperation = `\` + case TreeNodeNot: + setOperation = `not` + case TreeNodeTupleToSubjectSet: + setOperation = "┐ tuple to userset" + case TreeNodeComputedSubjectSet: + setOperation = "┐ computed userset" + } + + boxSymbol := "├" + if len(children) == 1 { + boxSymbol = "└" + } + return fmt.Sprintf("%s %s\n%s──%s", setOperation, nodeLabel, boxSymbol, strings.Join(children, "\n└──")) +} diff --git a/ketoapi/enc_test.go b/ketoapi/enc_test.go new file mode 100644 index 0000000..2b8ef2b --- /dev/null +++ b/ketoapi/enc_test.go @@ -0,0 +1,413 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +func TestRelationTuple(t *testing.T) { + t.Run("method=string encoding", func(t *testing.T) { + for _, tc := range []struct { + tuple *RelationTuple + expected string + }{ + { // full tuple + tuple: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + expected: "n:o#r@s", + }, + { // skip '#' in subject set when relation is empty + tuple: &RelationTuple{ + Namespace: "groups", + Object: "dev", + Relation: "member", + SubjectSet: &SubjectSet{ + Namespace: "users", + Object: "user", + }, + }, + expected: "groups:dev#member@users:user", + }, + } { + t.Run("case="+tc.expected, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.tuple.String()) + }) + } + }) + + t.Run("method=string decoding", func(t *testing.T) { + for _, tc := range []struct { + enc string + err error + expected *RelationTuple + }{ + { + enc: "n:o#r@s", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + }, + { + enc: "n:o#r@n:o#r", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + Relation: "r", + }, + }, + }, + { + enc: "n:o#r@n:o#...", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + Relation: "...", + }, + }, + }, + { + enc: "n:o#r@n:o#", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + }, + }, + }, + { + enc: "n:o#r@n:o", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + }, + }, + }, + { + enc: "n:o#r@(n:o#r)", + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + Relation: "r", + }, + }, + }, + { + enc: "#dev:@ory#:working:@projects:keto#awesome", + expected: &RelationTuple{ + Namespace: "#dev", + Object: "@ory", + Relation: ":working:", + SubjectSet: &SubjectSet{ + Namespace: "projects", + Object: "keto", + Relation: "awesome", + }, + }, + }, + { + enc: "no-colon#in@this", + err: ErrMalformedInput(), + }, + { + enc: "no:hash-in@this", + err: ErrMalformedInput(), + }, + { + enc: "no:at#in-this", + err: ErrMalformedInput(), + }, + } { + t.Run(fmt.Sprintf("string=%s", tc.enc), func(t *testing.T) { + actual, err := (&RelationTuple{}).FromString(tc.enc) + assert.True(t, errors.Is(err, tc.err), "%+v", err) + assert.Equal(t, tc.expected, actual) + }) + } + }) + + t.Run("case=url encoding-decoding", func(t *testing.T) { + for i, r := range []*RelationTuple{ + { + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + { + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + }, + }, + { + SubjectID: new(""), + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + vals := r.ToURLQuery() + res, err := (&RelationTuple{}).FromURLQuery(vals) + require.NoError(t, err, "raw: %+v, enc: %+v", r, vals) + assert.Equal(t, r, res) + }) + } + }) + + t.Run("case=url decoding-encoding", func(t *testing.T) { + for i, v := range []url.Values{ + { + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_id": []string{"foo"}, + }, + { + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_set.namespace": []string{"sn"}, + "subject_set.object": []string{"so"}, + "subject_set.relation": []string{"sr"}, + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + rt, err := (&RelationTuple{}).FromURLQuery(v) + require.NoError(t, err) + q := rt.ToURLQuery() + assert.Equal(t, v, q) + }) + } + }) + + t.Run("case=proto decoding", func(t *testing.T) { + for i, tc := range []struct { + proto TupleData + expected *RelationTuple + err error + }{ + { + proto: &rts.RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + Subject: nil, + }, + err: ErrNilSubject(), + }, + { + proto: &rts.RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + Subject: &rts.Subject{ + Ref: &rts.Subject_Set{ + Set: &rts.SubjectSet{ + Namespace: "n", + Object: "o", + Relation: "r", + }, + }, + }, + }, + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "n", + Object: "o", + Relation: "r", + }, + }, + }, + { + proto: &rts.RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + Subject: &rts.Subject{ + Ref: &rts.Subject_Id{ + Id: "user", + }, + }, + }, + expected: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("user"), + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + actual, err := (&RelationTuple{}).FromDataProvider(tc.proto) + require.ErrorIs(t, err, tc.err) + assert.Equal(t, tc.expected, actual) + }) + } + }) + + t.Run("format=JSON", func(t *testing.T) { + t.Run("direction=encoding-decoding", func(t *testing.T) { + for _, tc := range []struct { + name string + rt *RelationTuple + expected string + }{ + { + name: "with subject ID", + rt: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectID: new("s"), + }, + expected: ` +{ + "namespace": "n", + "object": "o", + "relation": "r", + "subject_id": "s" +}`, + }, + { + name: "with subject set", + rt: &RelationTuple{ + Namespace: "n", + Object: "o", + Relation: "r", + SubjectSet: &SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + }, + }, + expected: ` +{ + "namespace": "n", + "object": "o", + "relation": "r", + "subject_set": { + "namespace": "sn", + "object": "so", + "relation": "sr" + } +}`, + }, + } { + t.Run("case="+tc.name, func(t *testing.T) { + raw, err := json.Marshal(tc.rt) + require.NoError(t, err) + assert.JSONEq(t, tc.expected, string(raw)) + + var dec RelationTuple + require.NoError(t, json.Unmarshal(raw, &dec)) + assert.Equal(t, tc.rt, &dec) + }) + } + }) + }) +} + +func TestRelationQuery(t *testing.T) { + t.Run("case=url encoding-decoding-encoding", func(t *testing.T) { + for i, tc := range []struct { + v url.Values + r *RelationQuery + }{ + { + v: url.Values{ + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_id": []string{"foo"}, + }, + r: &RelationQuery{ + Namespace: new("n"), + Object: new("o"), + Relation: new("r"), + SubjectID: new("foo"), + }, + }, + { + v: url.Values{ + "namespace": []string{"n"}, + "object": []string{"o"}, + "relation": []string{"r"}, + "subject_set.namespace": []string{"sn"}, + "subject_set.object": []string{"so"}, + "subject_set.relation": []string{"sr"}, + }, + r: &RelationQuery{ + Namespace: new("n"), + Object: new("o"), + Relation: new("r"), + SubjectSet: &SubjectSet{ + Namespace: "sn", + Object: "so", + Relation: "sr", + }, + }, + }, + { + v: url.Values{ + "namespace": []string{"n"}, + "relation": []string{"r"}, + }, + r: &RelationQuery{ + Namespace: new("n"), + Relation: new("r"), + }, + }, + } { + t.Run(fmt.Sprintf("case=%d", i), func(t *testing.T) { + enc := tc.r.ToURLQuery() + assert.Equal(t, tc.v, enc) + + dec, err := (&RelationQuery{}).FromURLQuery(tc.v) + require.NoError(t, err) + assert.Equal(t, tc.r, dec) + }) + } + }) +} diff --git a/ketoapi/enc_url_query.go b/ketoapi/enc_url_query.go new file mode 100644 index 0000000..059071c --- /dev/null +++ b/ketoapi/enc_url_query.go @@ -0,0 +1,127 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +import ( + "net/url" + + "github.com/pkg/errors" +) + +func (q *RelationQuery) FromURLQuery(query url.Values) (*RelationQuery, error) { + if q == nil { + q = &RelationQuery{} + } + + if query.Has("subject") { + return nil, ErrDroppedSubjectKey() + } + + // reset subject + q.SubjectID = nil + q.SubjectSet = nil + + switch { + case !query.Has(SubjectIDKey) && !query.Has(SubjectSetNamespaceKey) && !query.Has(SubjectSetObjectKey) && !query.Has(SubjectSetRelationKey): + // was not queried for the subject + case query.Has(SubjectIDKey) && (query.Has(SubjectSetNamespaceKey) || query.Has(SubjectSetObjectKey) || query.Has(SubjectSetRelationKey)): + return nil, ErrDuplicateSubject().WithDebugf("please provide either %s or all of %s, %s, and %s", SubjectIDKey, SubjectSetNamespaceKey, SubjectSetObjectKey, SubjectSetRelationKey) + case query.Has(SubjectIDKey): + q.SubjectID = new(query.Get(SubjectIDKey)) + case query.Has(SubjectSetNamespaceKey) && query.Has(SubjectSetObjectKey) && query.Has(SubjectSetRelationKey): + q.SubjectSet = &SubjectSet{ + Namespace: query.Get(SubjectSetNamespaceKey), + Object: query.Get(SubjectSetObjectKey), + Relation: query.Get(SubjectSetRelationKey), + } + default: + return nil, ErrIncompleteSubject() + } + + if query.Has(NamespaceKey) { + q.Namespace = new(query.Get(NamespaceKey)) + } + if query.Has(ObjectKey) { + q.Object = new(query.Get(ObjectKey)) + } + if query.Has(RelationKey) { + q.Relation = new(query.Get(RelationKey)) + } + + return q, nil +} + +func (q *RelationQuery) ToURLQuery() url.Values { + v := make(url.Values, 7) + + if q.Namespace != nil { + v.Add(NamespaceKey, *q.Namespace) + } + if q.Relation != nil { + v.Add(RelationKey, *q.Relation) + } + if q.Object != nil { + v.Add(ObjectKey, *q.Object) + } + if q.SubjectID != nil { + v.Add(SubjectIDKey, *q.SubjectID) + } else if q.SubjectSet != nil { + v.Add(SubjectSetNamespaceKey, q.SubjectSet.Namespace) + v.Add(SubjectSetObjectKey, q.SubjectSet.Object) + v.Add(SubjectSetRelationKey, q.SubjectSet.Relation) + } + + return v +} + +func (r *RelationTuple) FromURLQuery(query url.Values) (*RelationTuple, error) { + q, err := (&RelationQuery{}).FromURLQuery(query) + if err != nil { + return nil, err + } + if q.SubjectID == nil && q.SubjectSet == nil { + return nil, errors.WithStack(ErrNilSubject()) + } + if q.Namespace == nil || q.Object == nil || q.Relation == nil { + return nil, errors.WithStack(ErrIncompleteTuple()) + } + + r.Namespace = *q.Namespace + r.Object = *q.Object + r.Relation = *q.Relation + r.SubjectID = q.SubjectID + r.SubjectSet = q.SubjectSet + + return r, nil +} + +func (r *RelationTuple) ToURLQuery() url.Values { + return (&RelationQuery{ + Namespace: &r.Namespace, + Object: &r.Object, + Relation: &r.Relation, + SubjectID: r.SubjectID, + SubjectSet: r.SubjectSet, + }).ToURLQuery() +} + +func (s *SubjectSet) FromURLQuery(values url.Values) *SubjectSet { + if s == nil { + s = &SubjectSet{} + } + + s.Namespace = values.Get(NamespaceKey) + s.Relation = values.Get(RelationKey) + s.Object = values.Get(ObjectKey) + + return s +} + +func (s *SubjectSet) ToURLQuery() url.Values { + return url.Values{ + NamespaceKey: []string{s.Namespace}, + ObjectKey: []string{s.Object}, + RelationKey: []string{s.Relation}, + } +} diff --git a/ketoapi/public_api_definitions.go b/ketoapi/public_api_definitions.go new file mode 100644 index 0000000..c370ad7 --- /dev/null +++ b/ketoapi/public_api_definitions.go @@ -0,0 +1,282 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoapi + +import ( + "encoding/json" + "fmt" + + "github.com/ory/herodot" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" +) + +var ErrUnknownNodeType = errors.New("unknown node type") + +func ErrDroppedSubjectKey() *herodot.DefaultError { + return herodot.ErrBadRequest().WithDebug(`provide "subject_id" or "subject_set.*"; support for "subject" was dropped`) +} + +func ErrDuplicateSubject() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("exactly one of subject_set or subject_id has to be provided") +} + +func ErrIncompleteSubject() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError(`incomplete subject, provide "subject_id" or a complete "subject_set.*"`) +} + +func ErrNilSubject() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("subject is not allowed to be nil").WithDebug("Please provide a subject.") +} + +func ErrIncompleteTuple() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError(`incomplete tuple, provide "namespace", "object", "relation", and a subject`) +} + +// swagger:model namespace +type Namespace struct { + // Name of the namespace. + Name string `json:"name"` +} + +// Relationship +// +// swagger:model relationship +type RelationTuple struct { + // Namespace of the Relation Tuple + // + // required: true + Namespace string `json:"namespace"` + + // Object of the Relation Tuple + // + // required: true + Object string `json:"object"` + + // Relation of the Relation Tuple + // + // required: true + Relation string `json:"relation"` + + // SubjectID of the Relation Tuple + // + // Either SubjectSet or SubjectID can be provided. + SubjectID *string `json:"subject_id,omitempty"` + + // SubjectSet of the Relation Tuple + // + // Either SubjectSet or SubjectID can be provided. + // + // swagger:allOf + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// swagger:model subjectSet +type SubjectSet struct { + // Namespace of the Subject Set + // + // required: true + Namespace string `json:"namespace"` + + // Object of the Subject Set + // + // required: true + Object string `json:"object"` + + // Relation of the Subject Set + // + // required: true + Relation string `json:"relation"` +} + +// Relation Query +// +// swagger:model relationQuery +type RelationQuery struct { + // Namespace to query + Namespace *string `json:"namespace"` + + // Object to query + Object *string `json:"object"` + + // Relation to query + Relation *string `json:"relation"` + + // SubjectID to query + // + // Either SubjectSet or SubjectID can be provided. + SubjectID *string `json:"subject_id,omitempty"` + // SubjectSet to query + // + // Either SubjectSet or SubjectID can be provided. + // + // swagger:allOf + SubjectSet *SubjectSet `json:"subject_set,omitempty"` +} + +// Payload for patching a relationship +// +// swagger:model relationshipPatch +type PatchDelta struct { + Action PatchAction `json:"action"` + RelationTuple *RelationTuple `json:"relation_tuple"` +} + +// swagger:enum PatchAction +type PatchAction string + +const ( + ActionInsert PatchAction = "insert" + ActionDelete PatchAction = "delete" +) + +const ( + NamespaceKey = "namespace" + ObjectKey = "object" + RelationKey = "relation" + SubjectIDKey = "subject_id" + SubjectSetNamespaceKey = "subject_set.namespace" + SubjectSetObjectKey = "subject_set.object" + SubjectSetRelationKey = "subject_set.relation" +) + +var RelationQueryKeys = []string{ + NamespaceKey, + ObjectKey, + RelationKey, + SubjectIDKey, + SubjectSetNamespaceKey, + SubjectSetObjectKey, + SubjectSetRelationKey, + "subject", // We have a more specific error message for this key. +} + +// Paginated Relationship List +// +// swagger:model relationships +type GetResponse struct { + RelationTuples []*RelationTuple `json:"relation_tuples"` + // The opaque token to provide in a subsequent request + // to get the next page. It is the empty string iff this is + // the last page. + NextPageToken string `json:"next_page_token"` +} + +// Relationship Namespace List +// +// swagger:model relationshipNamespaces +type GetNamespacesResponse struct { + Namespaces []Namespace `json:"namespaces"` +} + +func (r *RelationTuple) ToLoggerFields() logrus.Fields { + fields := make(logrus.Fields, 7) + q := r.ToURLQuery() + for k := range q { + fields[k] = q.Get(k) + } + return fields +} + +func (r *RelationTuple) Validate() error { + if r.SubjectSet == nil && r.SubjectID == nil { + return errors.WithStack(ErrNilSubject()) + } + return nil +} + +// swagger:enum ExpandNodeType +type ExpandNodeType TreeNodeType + +// swagger:enum TreeNodeType +type TreeNodeType string + +const ( + TreeNodeUnion TreeNodeType = "union" + TreeNodeExclusion TreeNodeType = "exclusion" + TreeNodeIntersection TreeNodeType = "intersection" + TreeNodeLeaf TreeNodeType = "leaf" + TreeNodeTupleToSubjectSet TreeNodeType = "tuple_to_subject_set" + TreeNodeComputedSubjectSet TreeNodeType = "computed_subject_set" + TreeNodeNot TreeNodeType = "not" + TreeNodeUnspecified TreeNodeType = "unspecified" +) + +func (t *TreeNodeType) UnmarshalJSON(v []byte) error { + var s string + if err := json.Unmarshal(v, &s); err != nil { + return err + } + switch nt := TreeNodeType(s); nt { + case TreeNodeUnion, TreeNodeExclusion, TreeNodeIntersection, TreeNodeLeaf, TreeNodeTupleToSubjectSet, TreeNodeComputedSubjectSet, TreeNodeNot, TreeNodeUnspecified: + *t = nt + default: + return ErrUnknownNodeType + } + return nil +} + +type tuple[T any] interface { + fmt.Stringer + ToProto() *rts.RelationTuple + FromProto(*rts.RelationTuple) T +} + +// Tree is a generic tree of either internal relationships (with UUIDs for +// objects, etc.) or API relationships (with strings for objects, etc.). +type Tree[T tuple[T]] struct { + // Propagate all struct changes to `swaggerOnlyExpandTree` as well. + // The type of the node. + // + // required: true + Type TreeNodeType `json:"type"` + + // The children of the node, possibly none. + Children []*Tree[T] `json:"children,omitempty"` + + // The relation tuple the node represents. + Tuple T `json:"tuple"` +} + +// IMPORTANT: We need a manual instantiation of the generic Tree[T] for the +// OpenAPI spec, since go-swagger does not understand generics :(. +// This can be fixed by using grpc-gateway. + +// swagger:model expandedPermissionTree +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type swaggerOnlyExpandTree struct { + // The type of the node. + // + // required: true + Type TreeNodeType `json:"type"` + + // The children of the node, possibly none. + Children []*swaggerOnlyExpandTree `json:"children,omitempty"` + + // The relation tuple the node represents. + Tuple *RelationTuple `json:"tuple"` +} + +type ParseError struct { + Message string `json:"message"` + Start SourcePosition `json:"start"` + End SourcePosition `json:"end"` +} +type SourcePosition struct { + Line int `json:"Line"` + Col int `json:"column"` +} + +// CheckOPLSyntaxResponse represents the response for an OPL syntax check request. +// +// swagger:model checkOplSyntaxResult +type CheckOPLSyntaxResponse struct { + // The list of syntax errors + // + // required: false + Errors []*ParseError `json:"errors,omitempty"` +} diff --git a/ketoctx/options.go b/ketoctx/options.go new file mode 100644 index 0000000..8c153e1 --- /dev/null +++ b/ketoctx/options.go @@ -0,0 +1,151 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoctx + +import ( + "io/fs" + "net/http" + + "connectrpc.com/connect" + "github.com/ory/pop/v6" + "github.com/ory/x/contextx" + "github.com/ory/x/healthx" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/popx" +) + +type ( + Opts struct { + logger *logrusx.Logger + TracerWrapper TracerWrapper + contextualizer contextx.Contextualizer + httpMiddlewares []func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) + connectInterceptors []connect.Interceptor + migrationOpts []popx.MigrationBoxOption + readyCheckers healthx.ReadyCheckers + extraMigrations []fs.FS + inspect InspectFunc + dbOpts []func(details *pop.ConnectionDetails) + } + Option func(o *Opts) + TracerWrapper func(*otelx.Tracer) *otelx.Tracer + InspectFunc func(*pop.Connection) error +) + +// WithDBOptionsModifier adds database connection options that will be applied to the +// underlying connection. +func WithDBOptionsModifier(mods ...func(details *pop.ConnectionDetails)) Option { + return func(o *Opts) { + o.dbOpts = append(o.dbOpts, mods...) + } +} + +// WithLogger sets the logger. +func WithLogger(l *logrusx.Logger) Option { + return func(o *Opts) { o.logger = l } +} + +// WithTracerWrapper sets a function that wraps the tracer. +func WithTracerWrapper(wrapper TracerWrapper) Option { + return func(o *Opts) { o.TracerWrapper = wrapper } +} + +// WithContextualizer sets the contextualizer. +func WithContextualizer(ctxer contextx.Contextualizer) Option { + return func(o *Opts) { + o.contextualizer = ctxer + } +} + +// WithHTTPMiddlewares adds HTTP middlewares to the list of HTTP middlewares. +func WithHTTPMiddlewares(m ...func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc)) Option { + return func(o *Opts) { + o.httpMiddlewares = m + } +} + +func WithConnectInterceptors(i ...connect.Interceptor) Option { + return func(o *Opts) { + o.connectInterceptors = append(o.connectInterceptors, i...) + } +} + +// WithExtraMigrations adds additional database migrations. +func WithExtraMigrations(o ...fs.FS) Option { + return func(opts *Opts) { + opts.extraMigrations = append(opts.extraMigrations, o...) + } +} + +// WithMigrationOptions adds migration options to the list of migration options. +func WithMigrationOptions(o ...popx.MigrationBoxOption) Option { + return func(opts *Opts) { + opts.migrationOpts = o + } +} + +// WithReadinessCheck adds a new readness health checker to the list of +// checkers. Can be called multiple times. If the name is already taken, the +// checker will be overwritten. +func WithReadinessCheck(name string, rc healthx.ReadyChecker) Option { + return func(o *Opts) { + if o.readyCheckers == nil { + o.readyCheckers = make(healthx.ReadyCheckers) + } + o.readyCheckers[name] = rc + } +} + +func Inspect(f InspectFunc) Option { + return func(o *Opts) { + o.inspect = f + } +} + +func (o *Opts) Logger() *logrusx.Logger { + return o.logger +} + +func (o *Opts) Contextualizer() contextx.Contextualizer { + return o.contextualizer +} + +func (o *Opts) HTTPMiddlewares() []func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + return o.httpMiddlewares +} + +func (o *Opts) ConnectInterceptors() []connect.Interceptor { + return o.connectInterceptors +} + +func (o *Opts) ExtraMigrations() []fs.FS { + return o.extraMigrations +} + +func (o *Opts) MigrationOptions() []popx.MigrationBoxOption { + return o.migrationOpts +} + +func (o *Opts) ReadyCheckers() healthx.ReadyCheckers { + return o.readyCheckers +} + +func (o *Opts) Inspect() InspectFunc { + return o.inspect +} + +func (o *Opts) DBOptionsModifiers() []func(details *pop.ConnectionDetails) { + return o.dbOpts +} + +func Options(options ...Option) *Opts { + o := &Opts{ + contextualizer: &contextx.Default{}, + } + for _, opt := range options { + opt(o) + } + return o +} diff --git a/ketoctx/options_test.go b/ketoctx/options_test.go new file mode 100644 index 0000000..352065f --- /dev/null +++ b/ketoctx/options_test.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketoctx + +import ( + "testing" + + "github.com/ory/x/contextx" + "github.com/stretchr/testify/assert" +) + +func TestOptions(t *testing.T) { + t.Run("case=has default contextualizer", func(t *testing.T) { + assert.Equal(t, &contextx.Default{}, Options().Contextualizer()) + }) + + t.Run("case=overwrites contextualizer", func(t *testing.T) { + ctxer := &struct { + contextx.Default + x string + }{x: "x"} + + opts := Options(WithContextualizer(ctxer)) + assert.Equal(t, ctxer, opts.Contextualizer()) + }) +} diff --git a/ketodriver/driver.go b/ketodriver/driver.go new file mode 100644 index 0000000..ba5864a --- /dev/null +++ b/ketodriver/driver.go @@ -0,0 +1,21 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ketodriver + +import ( + "context" + _ "embed" + + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/internal/persistence/sql" + "github.com/ory/keto/ketoctx" + "github.com/spf13/pflag" +) + +var SQLMigrations = sql.Migrations + +// NewRegistry returns a new default keto registry. It is a simple wrapper around internal driver.NewDefaultRegistry. +func NewRegistry(ctx context.Context, flags *pflag.FlagSet, withoutNetwork bool, opts []ketoctx.Option) (driver.Registry, error) { + return driver.NewDefaultRegistry(ctx, flags, withoutNetwork, opts) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..cc5b0ba --- /dev/null +++ b/main.go @@ -0,0 +1,15 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "github.com/ory/x/profilex" + + "github.com/ory/keto/cmd" +) + +func main() { + defer profilex.Profile().Stop() + cmd.Execute() +} diff --git a/openapitools.json b/openapitools.json new file mode 100644 index 0000000..64f2cbb --- /dev/null +++ b/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.2.0" + } +} diff --git a/oryx/.gitignore b/oryx/.gitignore new file mode 100644 index 0000000..7c9bd24 --- /dev/null +++ b/oryx/.gitignore @@ -0,0 +1,8 @@ +.bin +vendor +.idea +coverage.txt +node_modules/ +**/*.pprof +**/memstats.*.txt +.vscode/settings.json diff --git a/oryx/.goimportsignore b/oryx/.goimportsignore new file mode 100644 index 0000000..a725465 --- /dev/null +++ b/oryx/.goimportsignore @@ -0,0 +1 @@ +vendor/ \ No newline at end of file diff --git a/oryx/.golangci.yml b/oryx/.golangci.yml new file mode 100644 index 0000000..7f5807e --- /dev/null +++ b/oryx/.golangci.yml @@ -0,0 +1,9 @@ +version: "2" + +linters: + enable: + - gosec + - errcheck + - ineffassign + - staticcheck + - unused diff --git a/oryx/.nancy-ignore b/oryx/.nancy-ignore new file mode 100644 index 0000000..e69de29 diff --git a/oryx/.prettierignore b/oryx/.prettierignore new file mode 100644 index 0000000..3d36f2d --- /dev/null +++ b/oryx/.prettierignore @@ -0,0 +1,5 @@ +.github/pull_request_template.md +clidoc/testdata/ +healthx/openapi/patch.yaml +.snapshots +fixtures diff --git a/oryx/.reference-ignore b/oryx/.reference-ignore new file mode 100644 index 0000000..eee2a89 --- /dev/null +++ b/oryx/.reference-ignore @@ -0,0 +1,3 @@ +**/node_modules +docs +CHANGELOG.md diff --git a/oryx/.schemas/corsx/viper.schema.json b/oryx/.schemas/corsx/viper.schema.json new file mode 100644 index 0000000..c7e61f6 --- /dev/null +++ b/oryx/.schemas/corsx/viper.schema.json @@ -0,0 +1,92 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/corsx/viper.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Cross Origin Resource Sharing (CORS)", + "description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "title": "Enable CORS", + "description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered." + }, + "allowed_origins": { + "title": "Allowed Origins", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "default": ["*"], + "uniqueItems": true, + "examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + }, + "allowed_methods": { + "type": "array", + "title": "Allowed HTTP Methods", + "description": "A list of methods the client is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ] + }, + "uniqueItems": true, + "default": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "allowed_headers": { + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "title": "Allowed Request HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Authorization", "Content-Type"] + }, + "exposed_headers": { + "description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "title": "Allowed Response HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Content-Type"] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow HTTP Credentials", + "default": false, + "description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates." + }, + "max_age": { + "type": "number", + "default": 0, + "title": "Maximum Age", + "description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age." + }, + "debug": { + "type": "boolean", + "default": false, + "title": "Enable Debugging", + "description": "Set to true to debug server side CORS issues." + } + }, + "additionalProperties": false +} diff --git a/oryx/.schemas/logrusx/viper.schema.json b/oryx/.schemas/logrusx/viper.schema.json new file mode 100644 index 0000000..8648e8d --- /dev/null +++ b/oryx/.schemas/logrusx/viper.schema.json @@ -0,0 +1,24 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/logrusx/viper.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Log", + "description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "type": "string", + "default": "info", + "enum": ["panic", "fatal", "error", "warn", "info", "debug"], + "title": "Level", + "description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL." + }, + "format": { + "type": "string", + "default": "text", + "enum": ["text", "json"], + "title": "Format", + "description": "The log format can either be text or JSON." + } + }, + "additionalProperties": false +} diff --git a/oryx/.schemas/profilingx/viper.schema.json b/oryx/.schemas/profilingx/viper.schema.json new file mode 100644 index 0000000..af66f30 --- /dev/null +++ b/oryx/.schemas/profilingx/viper.schema.json @@ -0,0 +1,8 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/profilingx/viper.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Profiling", + "description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "type": "string", + "enum": ["cpu", "mem"] +} diff --git a/oryx/.schemas/tlsx/viper.schema.json b/oryx/.schemas/tlsx/viper.schema.json new file mode 100644 index 0000000..2ba259c --- /dev/null +++ b/oryx/.schemas/tlsx/viper.schema.json @@ -0,0 +1,47 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/tlsx/viper.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "definitions": { + "source": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + } + }, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/source" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/source" + } + ] + } + } +} diff --git a/oryx/LICENSE b/oryx/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/oryx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + 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 "[]" + 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 + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + 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 + + 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/oryx/Makefile b/oryx/Makefile new file mode 100644 index 0000000..2dcaab6 --- /dev/null +++ b/oryx/Makefile @@ -0,0 +1,61 @@ +SHELL=/bin/bash -o pipefail + +export PATH := .bin:${PATH} + +.bin/ory: Makefile + curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.2.2 + touch .bin/ory + +.PHONY: format +format: .bin/ory node_modules + .bin/ory dev headers copyright --type=open-source --exclude=clidoc/ --exclude=hasherx/mocks_pkdbf2_test.go --exclude=josex/ --exclude=hasherx/ --exclude=jsonnetsecure/jsonnet.go + go tool goimports -w -local github.com/ory . + npm exec -- prettier --write . + +.bin/golangci-lint: Makefile + curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin v2.10.1 + +.bin/licenses: Makefile + curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/ci/master/licenses/install | sh + +licenses: .bin/licenses node_modules # checks open-source licenses + .bin/licenses + +.PHONY: test +test: + make resetdb + export TEST_DATABASE_POSTGRESQL=postgres://postgres:secret@127.0.0.1:3445/hydra?sslmode=disable; export TEST_DATABASE_COCKROACHDB=cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable; export TEST_DATABASE_MYSQL='mysql://root:secret@tcp(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true'; go test -count=1 ./... + +.PHONY: resetdb +resetdb: + docker kill hydra_test_database_mysql || true + docker kill hydra_test_database_postgres || true + docker kill hydra_test_database_cockroach || true + docker rm -f hydra_test_database_mysql || true + docker rm -f hydra_test_database_postgres || true + docker rm -f hydra_test_database_cockroach || true + docker run --rm --name hydra_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0 + docker run --rm --name hydra_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=hydra -d postgres:11.8 + docker run --rm --name hydra_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:latest-v25.4 start-single-node --insecure + +.PHONY: lint +lint: .bin/golangci-lint + .bin/golangci-lint run -v ./... + +.PHONY: migrations-render +migrations-render: .bin/ory + ory dev pop migration render networkx/migrations/templates networkx/migrations/sql + +.PHONY: migrations-render-replace +migrations-render-replace: .bin/ory + ory dev pop migration render -r networkx/migrations/templates networkx/migrations/sql + +.PHONY: mocks +mocks: + go tool mockgen -package hasherx_test -destination hasherx/mocks_argon2_test.go github.com/ory/x/hasherx Argon2Configurator + go tool mockgen -package hasherx_test -destination hasherx/mocks_bcrypt_test.go github.com/ory/x/hasherx BCryptConfigurator + go tool mockgen -package hasherx_test -destination hasherx/mocks_pkdbf2_test.go github.com/ory/x/hasherx PBKDF2Configurator + +node_modules: package-lock.json + npm ci + touch node_modules diff --git a/oryx/aead/aead_oss.go b/oryx/aead/aead_oss.go new file mode 100644 index 0000000..d68ac3b --- /dev/null +++ b/oryx/aead/aead_oss.go @@ -0,0 +1,23 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !commercial + +// Package aead provides the authenticated cipher (AEAD) that Ory uses to +// seal opaque payloads such as pagination tokens. +package aead + +import ( + "crypto/cipher" + + "github.com/pkg/errors" + "golang.org/x/crypto/chacha20poly1305" +) + +// New returns the XChaCha20-Poly1305 AEAD that seals opaque payloads under +// the given key. Its 192-bit random nonce imposes no practical limit on the +// number of payloads sealed per key. +func New(key [32]byte) (cipher.AEAD, error) { + a, err := chacha20poly1305.NewX(key[:]) + return a, errors.Wrap(err, "cannot create XChaCha20-Poly1305 AEAD") +} diff --git a/oryx/aead/legacy_secretbox.go b/oryx/aead/legacy_secretbox.go new file mode 100644 index 0000000..0f427f7 --- /dev/null +++ b/oryx/aead/legacy_secretbox.go @@ -0,0 +1,19 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package aead + +import "golang.org/x/crypto/nacl/secretbox" + +// OpenLegacySecretbox opens a payload sealed by the NaCl secretbox scheme +// that was used before this package existed: a 24-byte nonce prefixed to the +// ciphertext, without additional data. Remove it once all such payloads have +// expired. +func OpenLegacySecretbox(key [32]byte, raw []byte) ([]byte, bool) { + if len(raw) < 24 { + return nil, false + } + var nonce [24]byte + copy(nonce[:], raw[:24]) + return secretbox.Open(nil, raw[24:], &nonce, &key) +} diff --git a/oryx/assertx/assertx.go b/oryx/assertx/assertx.go new file mode 100644 index 0000000..dea236b --- /dev/null +++ b/oryx/assertx/assertx.go @@ -0,0 +1,58 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package assertx + +import ( + "bytes" + "encoding/json" + "strings" + "testing" + + "github.com/tidwall/sjson" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func PrettifyJSONPayload(t testing.TB, payload interface{}) string { + t.Helper() + o, err := json.MarshalIndent(payload, "", " ") + require.NoErrorf(t, err, "%+v", payload) + return string(o) +} + +func EqualAsJSON(t testing.TB, expected, actual interface{}, args ...interface{}) { + t.Helper() + var eb, ab bytes.Buffer + if len(args) == 0 { + args = []interface{}{PrettifyJSONPayload(t, actual)} + } + + require.NoError(t, json.NewEncoder(&eb).Encode(expected), args...) + require.NoError(t, json.NewEncoder(&ab).Encode(actual), args...) + assert.JSONEq(t, strings.TrimSpace(eb.String()), strings.TrimSpace(ab.String()), args...) +} + +func EqualAsJSONExcept(t testing.TB, expected, actual interface{}, except []string, args ...interface{}) { + t.Helper() + var eb, ab bytes.Buffer + if len(args) == 0 { + args = []interface{}{PrettifyJSONPayload(t, actual)} + } + + require.NoError(t, json.NewEncoder(&eb).Encode(expected), args...) + require.NoError(t, json.NewEncoder(&ab).Encode(actual), args...) + + var err error + ebs, abs := eb.String(), ab.String() + for _, k := range except { + ebs, err = sjson.Delete(ebs, k) + require.NoError(t, err) + + abs, err = sjson.Delete(abs, k) + require.NoError(t, err) + } + + assert.JSONEq(t, strings.TrimSpace(ebs), strings.TrimSpace(abs), args...) +} diff --git a/oryx/cachex/cache_control.go b/oryx/cachex/cache_control.go new file mode 100644 index 0000000..a653c20 --- /dev/null +++ b/oryx/cachex/cache_control.go @@ -0,0 +1,30 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cachex + +import ( + "net/http" + "strings" +) + +type CacheControl map[string]string + +// From Go generated SDK code. +func ParseCacheControl(headers http.Header) CacheControl { + cc := CacheControl{} + ccHeader := headers.Get("Cache-Control") + for _, part := range strings.Split(ccHeader, ",") { + part = strings.Trim(part, " ") + if part == "" { + continue + } + if strings.ContainsRune(part, '=') { + keyval := strings.Split(part, "=") + cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",") + } else { + cc[part] = "" + } + } + return cc +} diff --git a/oryx/cachex/ristretto.go b/oryx/cachex/ristretto.go new file mode 100644 index 0000000..89fa873 --- /dev/null +++ b/oryx/cachex/ristretto.go @@ -0,0 +1,67 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cachex + +import ( + "github.com/dgraph-io/ristretto/v2" + "github.com/prometheus/client_golang/prometheus" +) + +// RistrettoCollector collects Ristretto cache metrics. +type RistrettoCollector struct { + prefix string + metricsFunc func() *ristretto.Metrics +} + +// NewRistrettoCollector creates a new RistrettoCollector. +// +// To use this collector, you need to register it with a Prometheus registry: +// +// func main() { +// cache, _ := ristretto.NewCache(&ristretto.Config{ +// NumCounters: 1e7, +// MaxCost: 1 << 30, +// BufferItems: 64, +// }) +// collector := NewRistrettoCollector("prefix_", func() *ristretto.Metrics { +// return cache.Metrics +// }) +// prometheus.MustRegister(collector) +// } +func NewRistrettoCollector(prefix string, metricsFunc func() *ristretto.Metrics) *RistrettoCollector { + return &RistrettoCollector{ + prefix: prefix, + metricsFunc: metricsFunc, + } +} + +// Describe sends the super-set of all possible descriptors of metrics +// collected by this Collector. +func (c *RistrettoCollector) Describe(ch chan<- *prometheus.Desc) { + ch <- prometheus.NewDesc(c.prefix+"ristretto_hits", "Total number of cache hits", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_misses", "Total number of cache misses", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_ratio", "Cache hit ratio", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_keys_added", "Total number of keys added to the cache", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_cost_added", "Total cost of keys added to the cache", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_keys_evicted", "Total number of keys evicted from the cache", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_cost_evicted", "Total cost of keys evicted from the cache", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_sets_dropped", "Total number of sets dropped", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_sets_rejected", "Total number of sets rejected", nil, nil) + ch <- prometheus.NewDesc(c.prefix+"ristretto_gets_kept", "Total number of gets kept", nil, nil) +} + +// Collect is called by the Prometheus registry when collecting metrics. +func (c *RistrettoCollector) Collect(ch chan<- prometheus.Metric) { + metrics := c.metricsFunc() + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_hits", "Total number of cache hits", nil, nil), prometheus.GaugeValue, float64(metrics.Hits())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_misses", "Total number of cache misses", nil, nil), prometheus.GaugeValue, float64(metrics.Misses())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_ratio", "Cache hit ratio", nil, nil), prometheus.GaugeValue, metrics.Ratio()) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_keys_added", "Total number of keys added to the cache", nil, nil), prometheus.GaugeValue, float64(metrics.KeysAdded())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_cost_added", "Total cost of keys added to the cache", nil, nil), prometheus.GaugeValue, float64(metrics.CostAdded())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_keys_evicted", "Total number of keys evicted from the cache", nil, nil), prometheus.GaugeValue, float64(metrics.KeysEvicted())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_cost_evicted", "Total cost of keys evicted from the cache", nil, nil), prometheus.GaugeValue, float64(metrics.CostEvicted())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_sets_dropped", "Total number of sets dropped", nil, nil), prometheus.GaugeValue, float64(metrics.SetsDropped())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_sets_rejected", "Total number of sets rejected", nil, nil), prometheus.GaugeValue, float64(metrics.SetsRejected())) + ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_gets_kept", "Total number of gets kept", nil, nil), prometheus.GaugeValue, float64(metrics.GetsKept())) +} diff --git a/oryx/castx/castx.go b/oryx/castx/castx.go new file mode 100644 index 0000000..5abc962 --- /dev/null +++ b/oryx/castx/castx.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package castx + +import ( + "encoding/csv" + "fmt" + "reflect" + "strings" + + "github.com/spf13/cast" +) + +// ToFloatSlice casts an interface to a []float64 type. +func ToFloatSlice(i interface{}) []float64 { + f, _ := ToFloatSliceE(i) + return f +} + +// ToFloatSliceE casts an interface to a []float64 type. +func ToFloatSliceE(i interface{}) ([]float64, error) { + if i == nil { + return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i) + } + + switch v := i.(type) { + case []float64: + return v, nil + } + + kind := reflect.TypeOf(i).Kind() + switch kind { + case reflect.Slice, reflect.Array: + s := reflect.ValueOf(i) + a := make([]float64, s.Len()) + for j := range a { + val, err := cast.ToFloat64E(s.Index(j).Interface()) + if err != nil { + return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i) + } + a[j] = val + } + return a, nil + default: + return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i) + } +} + +// ToStringSlice casts an interface to a []string type and respects comma-separated values. +func ToStringSlice(i interface{}) []string { + s, _ := ToStringSliceE(i) + return s +} + +// ToStringSliceE casts an interface to a []string type and respects comma-separated values. +func ToStringSliceE(i interface{}) ([]string, error) { + switch s := i.(type) { + case string: + return parseCSV(s) + } + + return cast.ToStringSliceE(i) +} + +func parseCSV(v string) ([]string, error) { + return csv.NewReader(strings.NewReader(v)).Read() +} diff --git a/oryx/clidoc/generate.go b/oryx/clidoc/generate.go new file mode 100644 index 0000000..20daa66 --- /dev/null +++ b/oryx/clidoc/generate.go @@ -0,0 +1,78 @@ +package clidoc + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +// Generate generates markdown documentation for a cobra command and its children. +func Generate(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("command expects one argument which is the path to the output directory") + } + + return generate(cmd, args[0]) +} + +func trimExt(s string) string { + return strings.ReplaceAll(strings.TrimSuffix(s, filepath.Ext(s)), "_", "-") +} + +func generate(cmd *cobra.Command, dir string) error { + cmd.DisableAutoGenTag = true + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := generate(c, dir); err != nil { + return err + } + } + + basename := strings.Replace(cmd.CommandPath(), " ", "-", -1) + if err := os.MkdirAll(filepath.Join(dir), 0750); err != nil { + return err + } + + filename := filepath.Join(dir, basename) + ".md" + f, err := os.Create(filename) //#nosec:G304 + if err != nil { + return err + } + defer (func() { _ = f.Close() })() + + if _, err := io.WriteString(f, fmt.Sprintf(`--- +id: %s +title: %s +description: %s +--- + + +`, + basename, + cmd.CommandPath(), + cmd.CommandPath(), + )); err != nil { + return err + } + + var b bytes.Buffer + if err := GenMarkdownCustom(cmd, &b, trimExt); err != nil { + return err + } + + _, err = f.WriteString(b.String()) + return err +} diff --git a/oryx/clidoc/md_docs.go b/oryx/clidoc/md_docs.go new file mode 100644 index 0000000..9932475 --- /dev/null +++ b/oryx/clidoc/md_docs.go @@ -0,0 +1,201 @@ +//Copyright 2015 Red Hat Inc. All rights reserved. +// +// 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. + +package clidoc + +import ( + "bytes" + "fmt" + "html" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/ory/x/cmdx" + + "github.com/spf13/cobra" +) + +func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error { + flags := cmd.NonInheritedFlags() + flags.SetOutput(buf) + if flags.HasAvailableFlags() { + buf.WriteString("### Options\n\n```\n") + flags.PrintDefaults() + buf.WriteString("```\n\n") + } + + parentFlags := cmd.InheritedFlags() + parentFlags.SetOutput(buf) + if parentFlags.HasAvailableFlags() { + buf.WriteString("### Options inherited from parent commands\n\n```\n") + parentFlags.PrintDefaults() + buf.WriteString("```\n\n") + } + return nil +} + +// GenMarkdown creates markdown output. +func GenMarkdown(cmd *cobra.Command, w io.Writer) error { + return GenMarkdownCustom(cmd, w, func(s string) string { return s }) +} + +// GenMarkdownCustom creates custom markdown output. +func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error { + cmd.InitDefaultHelpCmd() + cmd.InitDefaultHelpFlag() + + buf := new(bytes.Buffer) + name := cmd.CommandPath() + + buf.WriteString("## " + html.EscapeString(name) + "\n\n") + buf.WriteString(fenceIndentedBlocks(cmd.Short) + "\n\n") + if len(cmd.Long) > 0 { + buf.WriteString("### Synopsis\n\n") + long, err := cmdx.TemplateCommandField(cmd, cmd.Long) + if err != nil { + buf.WriteString(fmt.Sprintf("\n\n", err.Error())) + long = cmd.Long + } + buf.WriteString(fenceIndentedBlocks(long) + "\n\n") + } + + if cmd.Runnable() { + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine())) + } + + if len(cmd.Example) > 0 { + buf.WriteString("### Examples\n\n") + example, err := cmdx.TemplateCommandField(cmd, cmd.Example) + if err != nil { + buf.WriteString(fmt.Sprintf("\n\n", err.Error())) + example = cmd.Example + } + buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", example)) + } + + if err := printOptions(buf, cmd, name); err != nil { + return err + } + if hasSeeAlso(cmd) { + buf.WriteString("### See also\n\n") + if cmd.HasParent() { + parent := cmd.Parent() + pname := parent.CommandPath() + link := pname + ".md" + link = strings.Replace(link, " ", "_", -1) + short := parent.Short + if short != "" { + short = fmt.Sprintf(" %s", short) + } + buf.WriteString(fmt.Sprintf("* [%s](%s)%s\n", pname, linkHandler(link), short)) + cmd.VisitParents(func(c *cobra.Command) { + if c.DisableAutoGenTag { + cmd.DisableAutoGenTag = c.DisableAutoGenTag + } + }) + } + + children := cmd.Commands() + sort.Sort(byName(children)) + + for _, child := range children { + if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() { + continue + } + cname := name + " " + child.Name() + link := cname + ".md" + link = strings.Replace(link, " ", "_", -1) + short := child.Short + if short != "" { + short = fmt.Sprintf(" - %s", short) + } + buf.WriteString(fmt.Sprintf("* [%s](%s)\t%s\n", cname, linkHandler(link), short)) + } + buf.WriteString("\n") + } + + _, err := buf.WriteTo(w) + return err +} + +// GenMarkdownTree will generate a markdown page for this command and all +// descendants in the directory given. The header may be nil. +// This function may not work correctly if your command names have `-` in them. +// If you have `cmd` with two subcmds, `sub` and `sub-third`, +// and `sub` has a subcommand called `third`, it is undefined which +// help output will be in the file `cmd-sub-third.1`. +func GenMarkdownTree(cmd *cobra.Command, dir string) error { + identity := func(s string) string { return s } + emptyStr := func(s string) string { return "" } + return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity) +} + +// GenMarkdownTreeCustom is the the same as GenMarkdownTree, but +// with custom filePrepender and linkHandler. +func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error { + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil { + return err + } + } + + basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md" + filename := filepath.Join(dir, basename) + f, err := os.Create(filename) //#nosec:G304) //#nosec:G304 + if err != nil { + return err + } + defer (func() { _ = f.Close() })() + + if _, err := io.WriteString(f, filePrepender(filename)); err != nil { + return err + } + if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil { + return err + } + return nil +} + +var indentedBlock = regexp.MustCompile(`(?m)^(?: {4}|\t).*(?:\n(?: {4}|\t).*)*`) + +func fenceIndentedBlocks(s string) string { + return indentedBlock.ReplaceAllStringFunc(s, func(block string) string { + // trim trailing newline to keep fence tidy + block = strings.TrimRight(block, "\n") + + // de-indent exactly one level for nicer fenced output + lines := strings.Split(block, "\n") + for i, ln := range lines { + switch { + case strings.HasPrefix(ln, " "): + lines[i] = ln[4:] + case strings.HasPrefix(ln, "\t"): + lines[i] = ln[1:] + } + } + b := strings.Join(lines, "\n") + + // guard against already fenced content + if strings.HasPrefix(strings.TrimSpace(b), "```") { + return block + } + return "```\n" + b + "\n```" + }) +} diff --git a/oryx/clidoc/testdata/hydra-client-admin.md b/oryx/clidoc/testdata/hydra-client-admin.md new file mode 100644 index 0000000..935c453 --- /dev/null +++ b/oryx/clidoc/testdata/hydra-client-admin.md @@ -0,0 +1,48 @@ +--- +id: hydra-client-admin +title: hydra client admin +description: hydra client admin +--- + + +## hydra client admin + +Foo bar baz bar + +``` +short with multiple +``` + + + +### Synopsis + +Run the admin server + +``` +<[some argument]> + <[some argument]> + <[some argument]> + <[some argument]> +<[some argument]> +``` + + +``` +hydra client admin [flags] +``` + +### Options + +``` + -h, --help help for admin +``` + +### See also + +* [hydra client](hydra-client) Run client commands + diff --git a/oryx/clidoc/testdata/hydra-client-public.md b/oryx/clidoc/testdata/hydra-client-public.md new file mode 100644 index 0000000..66f9c20 --- /dev/null +++ b/oryx/clidoc/testdata/hydra-client-public.md @@ -0,0 +1,36 @@ +--- +id: hydra-client-public +title: hydra client public +description: hydra client public +--- + + +## hydra client public + + + +### Synopsis + +Run the public server + +<[some argument]> + + +``` +hydra client public [flags] +``` + +### Options + +``` + -h, --help help for public +``` + +### See also + +* [hydra client](hydra-client) Run client commands + diff --git a/oryx/clidoc/testdata/hydra-client.md b/oryx/clidoc/testdata/hydra-client.md new file mode 100644 index 0000000..eb105cd --- /dev/null +++ b/oryx/clidoc/testdata/hydra-client.md @@ -0,0 +1,48 @@ +--- +id: hydra-client +title: hydra client +description: hydra client +--- + + +## hydra client + +Run client commands + +### Synopsis + +Manage OAuth2 clients + +<[some argument]> + + +``` +hydra client [flags] +``` + +### Examples + +``` +hydra client --whatever +``` + +### Options + +``` + -h, --help help for client +``` + +### See also + +* [hydra](hydra) +* [hydra client admin](hydra-client-admin) - Foo bar baz bar + + short with multiple + + +* [hydra client public](hydra-client-public) + diff --git a/oryx/clidoc/testdata/hydra-serve.md b/oryx/clidoc/testdata/hydra-serve.md new file mode 100644 index 0000000..d7a6fc4 --- /dev/null +++ b/oryx/clidoc/testdata/hydra-serve.md @@ -0,0 +1,36 @@ +--- +id: hydra-serve +title: hydra serve +description: hydra serve +--- + + +## hydra serve + + + +### Synopsis + +Manage the server + +<[some argument]> + + +``` +hydra serve [flags] +``` + +### Options + +``` + -h, --help help for serve +``` + +### See also + +* [hydra](hydra) + diff --git a/oryx/clidoc/testdata/hydra.md b/oryx/clidoc/testdata/hydra.md new file mode 100644 index 0000000..863017e --- /dev/null +++ b/oryx/clidoc/testdata/hydra.md @@ -0,0 +1,38 @@ +--- +id: hydra +title: hydra +description: hydra +--- + + +## hydra + + + +### Synopsis + +A sample text +root + +<[some argument]> + + +``` +hydra [flags] +``` + +### Options + +``` + -h, --help help for hydra +``` + +### See also + +* [hydra client](hydra-client) - Run client commands +* [hydra serve](hydra-serve) + diff --git a/oryx/clidoc/util.go b/oryx/clidoc/util.go new file mode 100644 index 0000000..e8e74a3 --- /dev/null +++ b/oryx/clidoc/util.go @@ -0,0 +1,40 @@ +// Copyright 2015 Red Hat Inc. All rights reserved. +// +// 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. + +package clidoc + +import ( + "github.com/spf13/cobra" +) + +// Test to see if we have a reason to print See Also information in docs +// Basically this is a test for a parent command or a subcommand which is +// both not deprecated and not the autogenerated help command. +func hasSeeAlso(cmd *cobra.Command) bool { + if cmd.HasParent() { + return true + } + for _, c := range cmd.Commands() { + if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() { + continue + } + return true + } + return false +} + +type byName []*cobra.Command + +func (s byName) Len() int { return len(s) } +func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() } diff --git a/oryx/clock/clock.go b/oryx/clock/clock.go new file mode 100644 index 0000000..68a2812 --- /dev/null +++ b/oryx/clock/clock.go @@ -0,0 +1,65 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package clock provides a small, injectable time source. Production code reads +// the current time through a Clock instead of calling time.Now() directly, so +// time-dependent behavior (such as flow expiry) can be tested deterministically. +package clock + +import ( + "sync" + "time" +) + +// Clock is a source of the current time. +type Clock interface { + Now() time.Time +} + +// Provider is implemented by dependency containers (such as the Kratos registry) +// that expose a Clock. Embed it in a component's dependency interface to read +// the current time through the injected clock. +type Provider interface { + Clock() Clock +} + +// System is the production Clock backed by the real wall clock. +type System struct{} + +// Now returns the current wall-clock time. +func (System) Now() time.Time { return time.Now() } + +// New returns the production system clock. +func New() Clock { return System{} } + +// Mock is a Clock whose time only changes when the caller advances it. It is +// safe for concurrent use: a test goroutine may Add while a request goroutine +// reads Now. +type Mock struct { + mu sync.Mutex + t time.Time +} + +// NewMock returns a Mock fixed at the given instant. +func NewMock(t time.Time) *Mock { return &Mock{t: t} } + +// Now returns the Mock's current instant. +func (m *Mock) Now() time.Time { + m.mu.Lock() + defer m.mu.Unlock() + return m.t +} + +// Add advances the Mock by d. +func (m *Mock) Add(d time.Duration) { + m.mu.Lock() + defer m.mu.Unlock() + m.t = m.t.Add(d) +} + +// Set moves the Mock to the given instant. +func (m *Mock) Set(t time.Time) { + m.mu.Lock() + defer m.mu.Unlock() + m.t = t +} diff --git a/oryx/cmdx/env.go b/oryx/cmdx/env.go new file mode 100644 index 0000000..f7cb9c3 --- /dev/null +++ b/oryx/cmdx/env.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +// EnvVarExamplesHelpMessage returns a string containing documentation on how to use environment variables. +func EnvVarExamplesHelpMessage(name string) string { + return `This command exposes a variety of controls via environment variables. Here are some examples on how to +configure environment variables: + +Linux / macOS: + $ export FOO=bar + $ export BAZ=bar + $ ` + name + ` ... + + $ FOO=bar BAZ=bar ` + name + ` ... + +Docker: + $ docker run -e FOO=bar -e BAZ=bar ... + +Windows (cmd): + > set FOO=bar + > set BAZ=bar + > ` + name + ` ... + +Windows (powershell): + > $env:FOO = "bar" + > $env:BAZ = "bar" + > ` + name + ` +` +} diff --git a/oryx/cmdx/helper.go b/oryx/cmdx/helper.go new file mode 100644 index 0000000..5cf65e2 --- /dev/null +++ b/oryx/cmdx/helper.go @@ -0,0 +1,247 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "testing" + + "golang.org/x/sync/errgroup" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + + "github.com/pkg/errors" +) + +var ( + // ErrNoPrintButFail is returned to detect a failure state that was already reported to the user in some way + ErrNoPrintButFail = fmt.Errorf("this error should never be printed") + + debugStdout, debugStderr = io.Discard, io.Discard +) + +func init() { + if os.Getenv("DEBUG") != "" { + debugStdout = os.Stdout + debugStderr = os.Stderr + } +} + +// FailSilently is supposed to be used within a commands RunE function. +// It silences cobras error handling and returns the ErrNoPrintButFail error. +func FailSilently(cmd *cobra.Command) error { + cmd.SilenceErrors = true + cmd.SilenceUsage = true + return errors.WithStack(ErrNoPrintButFail) +} + +// Must fatals with the optional message if err is not nil. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func Must(err error, message string, args ...interface{}) { + if err == nil { + return + } + + _, _ = fmt.Fprintf(os.Stderr, message+"\n", args...) + os.Exit(1) +} + +// CheckResponse fatals if err is nil or the response.StatusCode does not match the expectedStatusCode +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func CheckResponse(err error, expectedStatusCode int, response *http.Response) { + Must(err, "Command failed because error occurred: %s", err) + + if response.StatusCode != expectedStatusCode { + out, err := io.ReadAll(response.Body) + if err != nil { + out = []byte{} + } + pretty, err := json.MarshalIndent(json.RawMessage(out), "", "\t") + if err == nil { + out = pretty + } + + Fatalf( + `Command failed because status code %d was expected but code %d was received. + +Response payload: + +%s`, + expectedStatusCode, + response.StatusCode, + out, + ) + } +} + +// FormatResponse takes an object and prints a json.MarshalIdent version of it or fatals. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func FormatResponse(o interface{}) string { + out, err := json.MarshalIndent(o, "", "\t") + Must(err, `Command failed because an error occurred while prettifying output: %s`, err) + return string(out) +} + +// Fatalf prints to os.Stderr and exists with code 1. +// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error. +func Fatalf(message string, args ...interface{}) { + if len(args) > 0 { + _, _ = fmt.Fprintf(os.Stderr, message+"\n", args...) + } else { + _, _ = fmt.Fprintln(os.Stderr, message) + } + os.Exit(1) +} + +// CallbackWriter will execute each callback once the message is received. +// The full matched message is passed to the callback. An error returned from the callback is returned by Write. +type CallbackWriter struct { + Callbacks map[string]func([]byte) error + buf bytes.Buffer +} + +func (c *CallbackWriter) Write(msg []byte) (int, error) { + for p, cb := range c.Callbacks { + if bytes.Contains(msg, []byte(p)) { + if err := cb(msg); err != nil { + return 0, err + } + } + } + return c.buf.Write(msg) +} + +func (c *CallbackWriter) String() string { + return c.buf.String() +} + +var _ io.Writer = (*CallbackWriter)(nil) + +func prepareCmd(cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args []string) { + cmd.SetIn(stdIn) + outs := []io.Writer{debugStdout} + if stdOut != nil { + outs = append(outs, stdOut) + } + cmd.SetOut(io.MultiWriter(outs...)) + errs := []io.Writer{debugStderr} + if stdErr != nil { + errs = append(errs, stdErr) + } + cmd.SetErr(io.MultiWriter(errs...)) + + if args == nil { + args = []string{} + } + cmd.SetArgs(args) +} + +// ExecBackgroundCtx runs the cobra command in the background. +func ExecBackgroundCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group { + prepareCmd(cmd, stdIn, stdOut, stdErr, args) + + eg := &errgroup.Group{} + eg.Go(func() error { + defer cmd.SetIn(nil) + return cmd.ExecuteContext(ctx) + }) + + return eg +} + +// Exec runs the provided cobra command with the given reader as STD_IN and the given args. +// Returns STD_OUT, STD_ERR and the error from the execution. +func Exec(t testing.TB, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) { + return ExecCtx(t.Context(), cmd, stdIn, args...) +} + +func ExecCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) { + stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{} + + prepareCmd(cmd, stdIn, stdOut, stdErr, args) + + // needs to be on a separate line to ensure that the output buffers are read AFTER the command ran + err := cmd.ExecuteContext(ctx) + + return stdOut.String(), stdErr.String(), err +} + +// ExecNoErr is a helper that assumes a successful run from Exec. +// Returns STD_OUT. +func ExecNoErr(t testing.TB, cmd *cobra.Command, args ...string) string { + return ExecNoErrCtx(t.Context(), t, cmd, args...) +} + +func ExecNoErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string { + stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...) + if err == nil { + require.Len(t, stdErr, 0, "std_out: %s\nstd_err: %s", stdOut, stdErr) + } else { + require.ErrorIsf(t, err, context.Canceled, "std_out: %s\nstd_err: %s", stdOut, stdErr) + } + return stdOut +} + +// ExecExpectedErr is a helper that assumes a failing run from Exec returning ErrNoPrintButFail +// Returns STD_ERR. +func ExecExpectedErr(t testing.TB, cmd *cobra.Command, args ...string) string { + return ExecExpectedErrCtx(t.Context(), t, cmd, args...) +} + +func ExecExpectedErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string { + stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...) + require.True(t, errors.Is(err, ErrNoPrintButFail), "std_out: %s\nstd_err: %s", stdOut, stdErr) + require.Len(t, stdOut, 0, stdErr) + return stdErr +} + +type CommandExecuter struct { + New func() *cobra.Command + Ctx context.Context + PersistentArgs []string +} + +func (c *CommandExecuter) Exec(stdin io.Reader, args ...string) (string, string, error) { + return ExecCtx(c.Ctx, c.New(), stdin, append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecBackground(stdin io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group { + return ExecBackgroundCtx(c.Ctx, c.New(), stdin, stdOut, stdErr, append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecNoErr(t require.TestingT, args ...string) string { + return ExecNoErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...) +} + +func (c *CommandExecuter) ExecExpectedErr(t require.TestingT, args ...string) string { + return ExecExpectedErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...) +} + +type URL struct { + url.URL +} + +var _ pflag.Value = (*URL)(nil) + +func (u *URL) Set(s string) error { + uu, err := url.Parse(s) + if err != nil { + return err + } + u.URL = *uu + return nil +} + +func (*URL) Type() string { + return "url" +} diff --git a/oryx/cmdx/http.go b/oryx/cmdx/http.go new file mode 100644 index 0000000..890884c --- /dev/null +++ b/oryx/cmdx/http.go @@ -0,0 +1,125 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "crypto/tls" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/ory/x/httpx" +) + +const ( + envKeyEndpoint = "ORY_SDK_URL" + FlagEndpoint = "endpoint" + FlagSkipTLSVerify = "skip-tls-verify" + FlagHeaders = "http-header" +) + +// Remote returns the remote endpoint for the given command. +func Remote(cmd *cobra.Command) (string, error) { + endpoint, err := cmd.Flags().GetString(FlagEndpoint) + if err != nil { + return "", errors.WithStack(err) + } + + if endpoint != "" { + return strings.TrimRight(endpoint, "/"), nil + } else if endpoint := os.Getenv("ORY_SDK_URL"); endpoint != "" { + return strings.TrimRight(endpoint, "/"), nil + } + + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To execute this command, the endpoint URL must point to the URL where Ory is located. To set the endpoint URL, use flag `--endpoint` or environment variable `ORY_SDK_URL`.") + return "", FailSilently(cmd) +} + +// RemoteURI returns the remote URI for the given command. +func RemoteURI(cmd *cobra.Command) (*url.URL, error) { + remote, err := Remote(cmd) + if err != nil { + return nil, err + } + + endpoint, err := url.ParseRequestURI(remote) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse endpoint URL: %s", err) + return nil, err + } + + return endpoint, nil +} + +// NewClient creates a new HTTP client. +func NewClient(cmd *cobra.Command) (*http.Client, *url.URL, error) { + endpoint, err := cmd.Flags().GetString(FlagEndpoint) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + if endpoint == "" { + endpoint = os.Getenv(envKeyEndpoint) + } + + if endpoint == "" { + return nil, nil, errors.Errorf("you have to set the remote endpoint, try --help for details") + } + + u, err := url.Parse(strings.TrimRight(endpoint, "/")) + if err != nil { + return nil, nil, errors.Wrapf(err, `could not parse the endpoint URL "%s"`, endpoint) + } + + hc := retryablehttp.NewClient().StandardClient() + hc.Timeout = time.Second * 10 + + rawHeaders, err := cmd.Flags().GetStringSlice(FlagHeaders) + if err != nil { + return nil, nil, errors.WithStack(err) + } + header := http.Header{} + for _, h := range rawHeaders { + parts := strings.Split(h, ":") + if len(parts) != 2 { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Unable to parse `--http-header` flag. Format of flag value is a `: ` delimited string like `--http-header 'Some-Header: some-values; other values`. Received: %v", rawHeaders) + return nil, nil, FailSilently(cmd) + } + + for k := range parts { + parts[k] = strings.TrimSpace(parts[k]) + } + + header.Add(parts[0], parts[1]) + } + + skipVerify, err := cmd.Flags().GetBool(FlagSkipTLSVerify) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + rt := httpx.NewTransportWithHeader(header) + rt.RoundTripper = &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: skipVerify, //nolint:gosec // This is a false positive + }, + } + hc.Transport = rt + return hc, u, nil +} + +// RegisterHTTPClientFlags registers HTTP client configuration flags. +func RegisterHTTPClientFlags(flags *pflag.FlagSet) { + flags.StringP(FlagEndpoint, FlagEndpoint[:1], "", fmt.Sprintf("The API URL this command should target. Alternatively set using the %s environmental variable.", envKeyEndpoint)) + flags.Bool(FlagSkipTLSVerify, false, "Do not verify TLS certificates. Useful when dealing with self-signed certificates. Do not use in production!") + flags.StringSliceP(FlagHeaders, "H", []string{}, "A list of additional HTTP headers to set. HTTP headers is separated by a `: `, for example: `-H 'Authorization: bearer some-token'`.") +} diff --git a/oryx/cmdx/noise_printer.go b/oryx/cmdx/noise_printer.go new file mode 100644 index 0000000..fbd46c9 --- /dev/null +++ b/oryx/cmdx/noise_printer.go @@ -0,0 +1,137 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type ConditionalPrinter struct { + w io.Writer + print bool +} + +const ( + FlagQuiet = "quiet" +) + +func RegisterNoiseFlags(flags *pflag.FlagSet) { + flags.BoolP(FlagQuiet, FlagQuiet[:1], false, "Be quiet with output printing.") +} + +// NewLoudOutPrinter returns a ConditionalPrinter that +// only prints to cmd.OutOrStdout when --quiet is not set +func NewLoudOutPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.OutOrStdout(), + print: !quiet, + } +} + +// NewQuietOutPrinter returns a ConditionalPrinter that +// only prints to cmd.OutOrStdout when --quiet is set +func NewQuietOutPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.OutOrStdout(), + print: quiet, + } +} + +// NewLoudErrPrinter returns a ConditionalPrinter that +// only prints to cmd.ErrOrStderr when --quiet is not set +func NewLoudErrPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.ErrOrStderr(), + print: !quiet, + } +} + +// NewQuietErrPrinter returns a ConditionalPrinter that +// only prints to cmd.ErrOrStderr when --quiet is set +func NewQuietErrPrinter(cmd *cobra.Command) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: cmd.ErrOrStderr(), + print: quiet, + } +} + +// NewLoudPrinter returns a ConditionalPrinter that +// only prints to w when --quiet is not set +func NewLoudPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: w, + print: !quiet, + } +} + +// NewQuietPrinter returns a ConditionalPrinter that +// only prints to w when --quiet is set +func NewQuietPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter { + quiet, err := cmd.Flags().GetBool(FlagQuiet) + if err != nil { + Fatalf(err.Error()) + } + + return &ConditionalPrinter{ + w: w, + print: quiet, + } +} + +func NewConditionalPrinter(w io.Writer, print bool) *ConditionalPrinter { + return &ConditionalPrinter{ + w: w, + print: print, + } +} + +func (p *ConditionalPrinter) Println(a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprintln(p.w, a...) + } + return +} + +func (p *ConditionalPrinter) Print(a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprint(p.w, a...) + } + return +} + +func (p *ConditionalPrinter) Printf(format string, a ...interface{}) (n int, err error) { + if p.print { + return fmt.Fprintf(p.w, format, a...) + } + return +} diff --git a/oryx/cmdx/output.go b/oryx/cmdx/output.go new file mode 100644 index 0000000..b17d46b --- /dev/null +++ b/oryx/cmdx/output.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import "strconv" + +type ( + // OutputIder outputs an ID + OutputIder string + // OutputIderCollection outputs a list of IDs + OutputIderCollection struct { + Items []OutputIder + } +) + +func (OutputIder) Header() []string { + return []string{"ID"} +} + +func (i OutputIder) Columns() []string { + return []string{string(i)} +} + +func (i OutputIder) Interface() interface{} { + return i +} + +func (OutputIderCollection) Header() []string { + return []string{"ID"} +} + +func (c OutputIderCollection) Table() [][]string { + rows := make([][]string, len(c.Items)) + for i, ident := range c.Items { + rows[i] = []string{string(ident)} + } + return rows +} + +func (c OutputIderCollection) Interface() interface{} { + return c.Items +} + +func (c OutputIderCollection) Len() int { + return len(c.Items) +} + +type PaginatedList struct { + Collection interface { + Table + IDs() []string + } `json:"-"` + Items []interface{} `json:"items"` + NextPageToken string `json:"next_page_token"` + IsLastPage bool `json:"is_last_page"` +} + +func (r *PaginatedList) Header() []string { + return r.Collection.Header() +} + +func (r *PaginatedList) Table() [][]string { + return append( + r.Collection.Table(), + []string{}, + []string{"NEXT PAGE TOKEN", r.NextPageToken}, + []string{"IS LAST PAGE", strconv.FormatBool(r.IsLastPage)}, + ) +} + +func (r *PaginatedList) Interface() interface{} { + return r +} + +func (r *PaginatedList) Len() int { + return r.Collection.Len() + 3 +} + +func (r *PaginatedList) IDs() []string { + return r.Collection.IDs() +} + +var _ Table = (*PaginatedList)(nil) diff --git a/oryx/cmdx/pagination.go b/oryx/cmdx/pagination.go new file mode 100644 index 0000000..b5c721a --- /dev/null +++ b/oryx/cmdx/pagination.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "strconv" + + "github.com/spf13/cobra" +) + +const ( + FlagPageSize = "page-size" + FlagPageToken = "page-token" +) + +func RegisterTokenPaginationFlags(cmd *cobra.Command) (pageSize int, pageToken string) { + cmd.Flags().StringVar(&pageToken, FlagPageToken, "", "page token acquired from a previous response") + cmd.Flags().IntVar(&pageSize, FlagPageSize, 100, "maximum number of items to return") + return +} + +// ParsePaginationArgs parses pagination arguments from the command line. +func ParsePaginationArgs(cmd *cobra.Command, pageArg, perPageArg string) (page, perPage int64, err error) { + if len(pageArg+perPageArg) > 0 { + page, err = strconv.ParseInt(pageArg, 0, 64) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse page argument\"%s\": %s", pageArg, err) + return 0, 0, FailSilently(cmd) + } + + perPage, err = strconv.ParseInt(perPageArg, 0, 64) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse per-page argument\"%s\": %s", perPageArg, err) + return 0, 0, FailSilently(cmd) + } + } + return +} + +// ParseTokenPaginationArgs parses token-based pagination arguments from the command line. +func ParseTokenPaginationArgs(cmd *cobra.Command) (page string, perPage int, err error) { + pageArg, err := cmd.Flags().GetString(FlagPageToken) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%s\": %s", FlagPageToken, pageArg, err) + return "", 0, FailSilently(cmd) + } + + perPageArg, err := cmd.Flags().GetInt(FlagPageSize) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%d\": %s", FlagPageSize, perPageArg, err) + return "", 0, FailSilently(cmd) + } + + return pageArg, perPageArg, nil +} diff --git a/oryx/cmdx/printing.go b/oryx/cmdx/printing.go new file mode 100644 index 0000000..b686d7d --- /dev/null +++ b/oryx/cmdx/printing.go @@ -0,0 +1,355 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + + "github.com/go-openapi/jsonpointer" + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/tidwall/gjson" +) + +type ( + TableHeader interface { + Header() []string + } + TableRow interface { + TableHeader + Columns() []string + Interface() interface{} + } + Table interface { + TableHeader + Table() [][]string + Interface() interface{} + Len() int + } + Nil struct{} + + Format string +) + +const ( + FormatQuiet Format = "quiet" + FormatTable Format = "table" + FormatJSON Format = "json" + FormatJSONPath Format = "jsonpath" + FormatJSONPointer Format = "jsonpointer" + FormatJSONPretty Format = "json-pretty" + FormatYAML Format = "yaml" + FormatDefault Format = "default" + + FlagFormat = "format" + + None = "" +) + +func (Nil) String() string { + return "null" +} + +func (Nil) Interface() interface{} { + return nil +} + +type printOptions struct { + format string +} + +type PrintOption func(*printOptions) + +func WithFormat(v string) PrintOption { + return func(o *printOptions) { + o.format = v + } +} + +func PrintErrors(cmd *cobra.Command, errs map[string]error) { + for src, err := range errs { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: %s\n", src, err.Error()) + } +} + +func PrintRow(cmd *cobra.Command, row TableRow) { + PrintRowf(cmd.OutOrStdout(), row, WithFormat(getFormatValue(cmd))) +} + +func PrintRowf(w io.Writer, row TableRow, opts ...PrintOption) { + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + switch parseFormat(o.format) { + case FormatQuiet: + if idAble, ok := row.(interface{ ID() string }); ok { + _, _ = fmt.Fprintln(w, idAble.ID()) + break + } + _, _ = fmt.Fprintln(w, row.Columns()[0]) + case FormatJSON: + printJSON(w, row.Interface(), false, "") + case FormatYAML: + printYAML(w, row.Interface()) + case FormatJSONPretty: + printJSON(w, row.Interface(), true, "") + case FormatJSONPath: + printJSON(w, row.Interface(), true, getPath(o.format)) + case FormatJSONPointer: + printJSON(w, filterJSONPointer(o.format, row.Interface()), true, "") + case FormatTable, FormatDefault: + w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0) + + fields := row.Columns() + for i, h := range row.Header() { + _, _ = fmt.Fprintf(w, "%s\t%s\t\n", h, fields[i]) + } + + _ = w.Flush() + } +} + +func filterJSONPointer(f string, data any) any { + _, jsonptr, found := strings.Cut(f, "=") + if !found { + _, _ = fmt.Fprintf(os.Stderr, + "Format %s is missing a JSON pointer, e.g., --%s=%s=. The path syntax is described at https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-07.", + f, FlagFormat, f) + os.Exit(1) + } + ptr, err := jsonpointer.New(jsonptr) + Must(err, "invalid JSON pointer: %s", err) + + result, _, err := ptr.Get(data) + Must(err, "failed to apply JSON pointer: %s", err) + + return result +} + +func PrintTable(cmd *cobra.Command, table Table) { + PrintTablef(cmd.OutOrStdout(), table, WithFormat(getFormatValue(cmd))) +} + +func PrintTablef(w io.Writer, table Table, opts ...PrintOption) { + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + switch parseFormat(o.format) { + case FormatQuiet: + if table.Len() == 0 { + fmt.Fprintln(w) + } + + if idAble, ok := table.(interface{ IDs() []string }); ok { + for _, row := range idAble.IDs() { + fmt.Fprintln(w, row) + } + break + } + + for _, row := range table.Table() { + fmt.Fprintln(w, row[0]) + } + case FormatJSON: + printJSON(w, table.Interface(), false, "") + case FormatJSONPretty: + printJSON(w, table.Interface(), true, "") + case FormatJSONPath: + printJSON(w, table.Interface(), true, getPath(o.format)) + case FormatJSONPointer: + printJSON(w, filterJSONPointer(o.format, table.Interface()), true, "") + case FormatYAML: + printYAML(w, table.Interface()) + default: + w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0) + + for _, h := range table.Header() { + fmt.Fprintf(w, "%s\t", h) + } + fmt.Fprintln(w) + + for _, row := range table.Table() { + fmt.Fprintln(w, strings.Join(row, "\t")+"\t") + } + + _ = w.Flush() + } +} + +type interfacer interface{ Interface() interface{} } + +func PrintJSONAble(cmd *cobra.Command, d interface{ String() string }) { + PrintJSONAblef(cmd.OutOrStdout(), d, WithFormat(getFormatValue(cmd))) +} + +func PrintJSONAblef(w io.Writer, d interface{ String() string }, opts ...PrintOption) { + if d == nil { + d = Nil{} + } + + o := &printOptions{} + for _, fn := range opts { + fn(o) + } + + var path string + switch parseFormat(o.format) { + default: + _, _ = fmt.Fprint(w, d.String()) + case FormatJSON: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, v, false, "") + case FormatJSONPath: + path = getPath(o.format) + fallthrough + case FormatJSONPretty: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, v, true, path) + case FormatJSONPointer: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printJSON(w, filterJSONPointer(o.format, v), true, "") + case FormatYAML: + var v interface{} = d + if i, ok := d.(interfacer); ok { + v = i + } + printYAML(w, v) + } +} + +func getQuiet(cmd *cobra.Command) bool { + // ignore the error here as we use this function also when the flag might not be registered + q, _ := cmd.Flags().GetBool(FlagQuiet) + return q +} + +func getFormatValue(cmd *cobra.Command) string { + if getQuiet(cmd) { + return string(FormatQuiet) + } + + f, _ := cmd.Flags().GetString(FlagFormat) + return f +} + +func parseFormat(f string) Format { + switch { + case f == string(FormatQuiet): + return FormatQuiet + case f == string(FormatTable): + return FormatTable + case f == string(FormatJSON): + return FormatJSON + case strings.HasPrefix(f, string(FormatJSONPath)): + return FormatJSONPath + case strings.HasPrefix(f, string(FormatJSONPointer)): + return FormatJSONPointer + case f == string(FormatJSONPretty): + return FormatJSONPretty + case f == string(FormatYAML): + return FormatYAML + default: + return FormatDefault + } +} + +func getPath(f string) string { + _, path, found := strings.Cut(f, "=") + if !found { + _, _ = fmt.Fprintf(os.Stderr, + "Format %s is missing a path, e.g., --%s=%s=. The path syntax is described at https://github.com/tidwall/gjson/blob/master/SYNTAX.md", + f, FlagFormat, f) + os.Exit(1) + } + + return path +} + +func printJSON(w io.Writer, v interface{}, pretty bool, path string) { + if path != "" { + temp, err := json.Marshal(v) + Must(err, "Error encoding JSON: %s", err) + v = gjson.GetBytes(temp, path).Value() + } + + e := json.NewEncoder(w) + if pretty { + e.SetIndent("", " ") + } + err := e.Encode(v) + // unexpected error + Must(err, "Error encoding JSON: %s", err) +} + +func printYAML(w io.Writer, v interface{}) { + j, err := json.Marshal(v) + Must(err, "Error encoding JSON: %s", err) + e, err := yaml.JSONToYAML(j) + Must(err, "Error encoding YAML: %s", err) + _, _ = w.Write(e) +} + +func RegisterJSONFormatFlags(flags *pflag.FlagSet) { + flags.String(FlagFormat, string(FormatDefault), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatDefault, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer)) +} + +func RegisterFormatFlags(flags *pflag.FlagSet) { + RegisterNoiseFlags(flags) + flags.String(FlagFormat, string(FormatTable), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatTable, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer)) +} + +func PrintOpenAPIError(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + + var be interface { + Body() []byte + } + if !errors.As(err, &be) { + return err + } + + body := be.Body() + didPrettyPrint := false + if message := gjson.GetBytes(body, "error.message"); message.Exists() { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", message.String()) + didPrettyPrint = true + } + if reason := gjson.GetBytes(body, "error.reason"); reason.Exists() { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", reason.String()) + didPrettyPrint = true + } + + if didPrettyPrint { + return FailSilently(cmd) + } + + if body, err := json.MarshalIndent(json.RawMessage(body), "", " "); err == nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\nFailed to execute API request, see error above.\n", body) + return FailSilently(cmd) + } + + return err +} diff --git a/oryx/cmdx/usage.go b/oryx/cmdx/usage.go new file mode 100644 index 0000000..891b8cd --- /dev/null +++ b/oryx/cmdx/usage.go @@ -0,0 +1,122 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bytes" + "maps" + "sync" + "text/template" + + "github.com/Masterminds/sprig/v3" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + usageTemplateFuncs = sprig.TxtFuncMap() + usageTemplateFuncsMux sync.RWMutex + enableUsageTemplatingOnce sync.Once +) + +// AddUsageTemplateFunc adds a template function to the usage template. +func AddUsageTemplateFunc(name string, f interface{}) { + usageTemplateFuncsMux.Lock() + usageTemplateFuncs[name] = f + usageTemplateFuncsMux.Unlock() +} + +const ( + helpTemplate = `{{insertTemplate . (or .Long .Short) | trimTrailingWhitespaces}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` + usageTemplate = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{insertTemplate . .Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Flags: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` +) + +// EnableUsageTemplating enables gotemplates for usage strings, i.e. cmd.Short, cmd.Long, and cmd.Example. +// The data for the template is the command itself. Especially useful are `.Root.Name` and `.CommandPath`. +// This will be inherited by all subcommands, so enabling it on the root command is sufficient. +func EnableUsageTemplating(cmds ...*cobra.Command) { + enableUsageTemplatingOnce.Do(func() { + cobra.AddTemplateFunc("insertTemplate", TemplateCommandField) + }) + for _, cmd := range cmds { + cmd.SetHelpTemplate(helpTemplate) + cmd.SetUsageTemplate(usageTemplate) + } +} + +func TemplateCommandField(cmd *cobra.Command, field string) (string, error) { + t := template.New("") + usageTemplateFuncsMux.RLock() + t.Funcs(maps.Clone(usageTemplateFuncs)) + usageTemplateFuncsMux.RUnlock() + t, err := t.Parse(field) + if err != nil { + return "", err + } + var out bytes.Buffer + if err := t.Execute(&out, cmd); err != nil { + return "", err + } + return out.String(), nil +} + +// DisableUsageTemplating resets the commands usage template to the default. +// This can be used to undo the effects of EnableUsageTemplating, specifically for a subcommand. +func DisableUsageTemplating(cmds ...*cobra.Command) { + defaultCmd := new(cobra.Command) + for _, cmd := range cmds { + cmd.SetHelpTemplate(defaultCmd.HelpTemplate()) + cmd.SetUsageTemplate(defaultCmd.UsageTemplate()) + } +} + +// AssertUsageTemplates asserts that the usage string of the commands are properly templated. +func AssertUsageTemplates(t require.TestingT, cmd *cobra.Command) { + var usage, help string + require.NotPanics(t, func() { + usage = cmd.UsageString() + + out, err := cmd.OutOrStdout(), cmd.ErrOrStderr() + bb := new(bytes.Buffer) + + cmd.SetOut(bb) + cmd.SetErr(bb) + require.NoError(t, cmd.Help()) + help = bb.String() + + cmd.SetOut(out) + cmd.SetErr(err) + }) + assert.NotContains(t, usage, "{{") + assert.NotContains(t, help, "{{") + for _, child := range cmd.Commands() { + AssertUsageTemplates(t, child) + } +} diff --git a/oryx/cmdx/user_input.go b/oryx/cmdx/user_input.go new file mode 100644 index 0000000..1659d29 --- /dev/null +++ b/oryx/cmdx/user_input.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" + + "github.com/pkg/errors" +) + +// asks for confirmation with the question string s and reads the answer +// pass nil to use os.Stdin and os.Stdout +func AskForConfirmation(s string, stdin io.Reader, stdout io.Writer) bool { + if stdin == nil { + stdin = os.Stdin + } + if stdout == nil { + stdout = os.Stdout + } + + ok, err := AskScannerForConfirmation(s, bufio.NewReader(stdin), stdout) + if err != nil { + Must(err, "Unable to confirm: %s", err) + } + + return ok +} + +func AskScannerForConfirmation(s string, reader *bufio.Reader, stdout io.Writer) (bool, error) { + if stdout == nil { + stdout = os.Stdout + } + + for { + _, err := fmt.Fprintf(stdout, "%s [y/n]: ", s) + if err != nil { + return false, errors.Wrap(err, "unable to print to stdout") + } + + response, err := reader.ReadString('\n') + if err != nil { + return false, errors.Wrap(err, "unable to read from stdin") + } + + response = strings.ToLower(strings.TrimSpace(response)) + if response == "y" || response == "yes" { + return true, nil + } else if response == "n" || response == "no" { + return false, nil + } + } +} diff --git a/oryx/cmdx/version.go b/oryx/cmdx/version.go new file mode 100644 index 0000000..8864161 --- /dev/null +++ b/oryx/cmdx/version.go @@ -0,0 +1,38 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cmdx + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// Version returns a *cobra.Command that handles the `version` command. +func Version(gitTag, gitHash, buildTime *string) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Show the build version, build time, and git hash", + Run: func(cmd *cobra.Command, args []string) { + if len(*gitTag) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine version because the build process did not properly configure it.") + } else { + fmt.Printf("Version: %s\n", *gitTag) + } + + if len(*gitHash) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine build commit because the build process did not properly configure it.") + } else { + fmt.Printf("Build Commit: %s\n", *gitHash) + } + + if len(*buildTime) == 0 { + fmt.Fprintln(os.Stderr, "Unable to determine build timestamp because the build process did not properly configure it.") + } else { + fmt.Printf("Build Timestamp: %s\n", *buildTime) + } + }, + } +} diff --git a/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json b/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/oryx/configx/.snapshots/TestKoanfSchemaDefaults.json @@ -0,0 +1 @@ +{} diff --git a/oryx/configx/context.go b/oryx/configx/context.go new file mode 100644 index 0000000..a465d36 --- /dev/null +++ b/oryx/configx/context.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import "context" + +type contextKey int + +const configContextKey contextKey = iota + 1 + +func ContextWithConfigOptions(ctx context.Context, opts ...OptionModifier) context.Context { + return context.WithValue(ctx, configContextKey, opts) +} + +func ConfigOptionsFromContext(ctx context.Context) []OptionModifier { + opts, ok := ctx.Value(configContextKey).([]OptionModifier) + if !ok { + return []OptionModifier{} + } + return opts +} diff --git a/oryx/configx/cors.go b/oryx/configx/cors.go new file mode 100644 index 0000000..cc9528d --- /dev/null +++ b/oryx/configx/cors.go @@ -0,0 +1,41 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + _ "embed" + "net/http" + + "github.com/rs/cors" + + "github.com/ory/x/corsx" +) + +const CORSConfigSchemaID = "ory://cors-config" + +//go:embed cors.schema.json +var CORSConfigSchema []byte + +func (p *Provider) CORS(prefix string, defaults cors.Options) (cors.Options, bool) { + prefix = cleanPrefix(prefix) + allowedOrigins := p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins) + allowInsecureOrigins := p.BoolF("feature_flags.legacy_allow_insecure_origins", false) + return cors.Options{ + // Populated even though rs/cors ignores it once AllowOriginVaryRequestFunc + // is set: some consumers read AllowedOrigins directly — notably hydra's + // oauth2cors.Middleware (which builds its own matcher and treats an empty + // list as "allow all", a CORS bypass) and oathkeeper's address detection. + AllowedOrigins: p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins), + AllowedMethods: p.StringsF(prefix+"cors.allowed_methods", defaults.AllowedMethods), + AllowedHeaders: p.StringsF(prefix+"cors.allowed_headers", defaults.AllowedHeaders), + ExposedHeaders: p.StringsF(prefix+"cors.exposed_headers", defaults.ExposedHeaders), + AllowCredentials: p.BoolF(prefix+"cors.allow_credentials", defaults.AllowCredentials), + OptionsPassthrough: p.BoolF(prefix+"cors.options_passthrough", defaults.OptionsPassthrough), + MaxAge: p.IntF(prefix+"cors.max_age", defaults.MaxAge), + Debug: p.BoolF(prefix+"cors.debug", defaults.Debug), + AllowOriginVaryRequestFunc: func(_ *http.Request, origin string) (bool, []string) { + return corsx.CheckOrigin(allowedOrigins, origin, allowInsecureOrigins), nil + }, + }, p.Bool(prefix + "cors.enabled") +} diff --git a/oryx/configx/cors.schema.json b/oryx/configx/cors.schema.json new file mode 100644 index 0000000..e65559a --- /dev/null +++ b/oryx/configx/cors.schema.json @@ -0,0 +1,106 @@ +{ + "$id": "https://github.com/ory/x/configx/cors.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "title": "CORS", + "description": "Configures Cross Origin Resource Sharing for this endpoint.", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: https://*.example.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "matches all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non-simple headers the client is allowed to use with cross-domain requests.", + "examples": [ + [ + "Authorization", + "Content-Type", + "Max-Age", + "X-Session-Token", + "X-XSRF-TOKEN", + "X-CSRF-TOKEN" + ] + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug CORS issues.", + "default": false + } + } +} diff --git a/oryx/configx/error.go b/oryx/configx/error.go new file mode 100644 index 0000000..d705092 --- /dev/null +++ b/oryx/configx/error.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "fmt" + + "github.com/pkg/errors" +) + +type ImmutableError struct { + From interface{} + To interface{} + Key string + error +} + +func NewImmutableError(key string, from, to interface{}) error { + return &ImmutableError{ + From: from, + To: to, + Key: key, + error: errors.Errorf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", key, from, to), + } +} + +func (e *ImmutableError) Error() string { + return fmt.Sprintf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", e.Key, e.From, e.To) +} diff --git a/oryx/configx/helpers.go b/oryx/configx/helpers.go new file mode 100644 index 0000000..8b9fc80 --- /dev/null +++ b/oryx/configx/helpers.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/spf13/pflag" +) + +// RegisterFlags registers the config file flag. +func RegisterFlags(flags *pflag.FlagSet) { + flags.StringSliceP("config", "c", []string{}, "Path to one or more .json, .yaml, .yml, .toml config files. Values are loaded in the order provided, meaning that the last config file overwrites values from the previous config file.") +} + +// host = unix:/path/to/socket => port is discarded, otherwise format as host:port +func GetAddress(host string, port int) string { + if strings.HasPrefix(host, "unix:") { + return host + } + return fmt.Sprintf("%s:%d", host, port) +} + +func (s *Serve) GetAddress() string { + return GetAddress(s.Host, s.Port) +} + +// AddSchemaResources adds the config schema partials to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddSchemaResources(c interface { + AddResource(url string, r io.Reader) error +}) error { + if err := c.AddResource(TLSConfigSchemaID, bytes.NewReader(TLSConfigSchema)); err != nil { + return err + } + if err := c.AddResource(ServeConfigSchemaID, bytes.NewReader(ServeConfigSchema)); err != nil { + return err + } + return c.AddResource(CORSConfigSchemaID, bytes.NewReader(CORSConfigSchema)) +} + +func cleanPrefix(prefix string) string { + if len(prefix) > 0 { + prefix = strings.TrimRight(prefix, ".") + "." + } + return prefix +} diff --git a/oryx/configx/koanf_confmap.go b/oryx/configx/koanf_confmap.go new file mode 100644 index 0000000..4824598 --- /dev/null +++ b/oryx/configx/koanf_confmap.go @@ -0,0 +1,69 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "encoding/json" + "errors" + + "github.com/knadh/koanf/maps" + "github.com/tidwall/gjson" +) + +// KoanfConfmap implements a raw map[string]interface{} provider. +type KoanfConfmap struct { + tuples []tuple +} + +// Provider returns a confmap Provider that takes a flat or nested +// map[string]interface{}. If a delim is provided, it indicates that the +// keys are flat and the map needs to be unflatted by delim. +func NewKoanfConfmap(tuples []tuple) *KoanfConfmap { + return &KoanfConfmap{tuples: jsonify(tuples)} +} + +func jsonify(tuples []tuple) []tuple { + for k, t := range tuples { + var parsed interface{} + switch vt := t.Value.(type) { + case string: + if gjson.Valid(vt) && json.NewDecoder(bytes.NewBufferString(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + case []byte: + if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + case json.RawMessage: + if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil { + tuples[k].Value = parsed + } + continue + } + } + return tuples +} + +// ReadBytes is not supported by the env provider. +func (e *KoanfConfmap) ReadBytes() ([]byte, error) { + return nil, errors.New("confmap provider does not support this method") +} + +// Read returns the loaded map[string]interface{}. +func (e *KoanfConfmap) Read() (map[string]interface{}, error) { + values := map[string]interface{}{} + for _, t := range e.tuples { + values[t.Key] = t.Value + } + + // Ensure any nested values are properly converted as well + cp := maps.Copy(values) + maps.IntfaceKeysToStrings(cp) + cp = maps.Unflatten(cp, Delimiter) + + return cp, nil +} diff --git a/oryx/configx/koanf_env.go b/oryx/configx/koanf_env.go new file mode 100644 index 0000000..0e97725 --- /dev/null +++ b/oryx/configx/koanf_env.go @@ -0,0 +1,185 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "encoding/json" + "os" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/jsonschema/v3" + + "github.com/spf13/cast" + "github.com/tidwall/gjson" + + "github.com/ory/x/castx" + "github.com/ory/x/jsonschemax" +) + +var isNumRegex = regexp.MustCompile("^[0-9]+$") + +func NewKoanfEnv(prefix string, rawSchema []byte, schema *jsonschema.Schema) (*Env, error) { + paths, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + + return &Env{ + paths: paths, + prefix: prefix, + }, nil +} + +// Env implements an environment variables provider. +type Env struct { + prefix string + paths []jsonschemax.Path +} + +// ReadBytes is not supported by the env provider. +func (e *Env) ReadBytes() ([]byte, error) { + return nil, errors.New("env provider does not support this method") +} + +// Read reads all available environment variables into a key:value map +// and returns it. +func (e *Env) Read() (map[string]interface{}, error) { + // Collect the environment variable keys. + var keys []string + for _, k := range os.Environ() { + if e.prefix != "" { + if strings.HasPrefix(k, e.prefix) { + keys = append(keys, k) + } + } else { + keys = append(keys, k) + } + } + + raw := "{}" + var err error + for _, k := range keys { + parts := strings.SplitN(k, "=", 2) + + key, value := e.extract(parts[0], parts[1]) + // If the callback blanked the key, it should be omitted + if key == "" { + continue + } + + raw, err = sjson.Set(raw, key, value) + if err != nil { + return nil, errors.WithStack(err) + } + } + + var m map[string]interface{} + if err := json.Unmarshal([]byte(raw), &m); err != nil { + return nil, errors.WithStack(err) + } + + return m, nil +} + +// Watch is not supported. +func (e *Env) Watch(cb func(event interface{}, err error)) error { + return errors.New("env provider does not support this method") +} + +func (e *Env) extract(key string, value string) (string, interface{}) { + key = strings.Replace(strings.ToLower(strings.TrimPrefix(key, e.prefix)), "_", ".", -1) + + for _, path := range e.paths { + normalized := strings.Replace(path.Name, "_", ".", -1) + name := path.Name + + // Crazy hack to get arrays working. + var indices []string + searchParts := strings.Split(normalized, ".") + keyParts := strings.Split(key, ".") + if len(searchParts) == len(keyParts) { + for k, search := range searchParts { + if search != keyParts[k] { + indices = nil + } + + if search != "#" { + continue + } + + if !isNumRegex.MatchString(keyParts[k]) { + continue + } + + searchParts[k] = keyParts[k] + indices = append(indices, keyParts[k]) + } + } + + if len(indices) > 0 { + normalized = strings.Join(searchParts, ".") + for _, index := range indices { + name = strings.Replace(name, "#", index, 1) + } + } + + if normalized == key { + switch path.TypeHint { + case jsonschemax.String: + return name, cast.ToString(value) + case jsonschemax.Float: + return name, cast.ToFloat64(value) + case jsonschemax.Int: + return name, cast.ToInt64(value) + case jsonschemax.Bool: + return name, cast.ToBool(value) + case jsonschemax.Nil: + return name, nil + case jsonschemax.BoolSlice: + if !gjson.Valid(value) { + return name, cast.ToBoolSlice(value) + } + fallthrough + case jsonschemax.StringSlice: + if !gjson.Valid(value) { + return name, castx.ToStringSlice(value) + } + fallthrough + case jsonschemax.IntSlice: + if !gjson.Valid(value) { + return name, cast.ToIntSlice(value) + } + fallthrough + case jsonschemax.FloatSlice: + if !gjson.Valid(value) { + return name, castx.ToFloatSlice(value) + } + fallthrough + case jsonschemax.JSON: + return name, decode(value) + default: + return name, value + } + } + } + + return "", nil +} + +func decode(value string) (v interface{}) { + b := []byte(value) + var arr []interface{} + if err := json.Unmarshal(b, &arr); err == nil { + return &arr + } + h := map[string]interface{}{} + if err := json.Unmarshal(b, &h); err == nil { + return &h + } + return nil +} diff --git a/oryx/configx/koanf_file.go b/oryx/configx/koanf_file.go new file mode 100644 index 0000000..a862df8 --- /dev/null +++ b/oryx/configx/koanf_file.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/v2" + + "github.com/pkg/errors" + + "github.com/ory/x/watcherx" +) + +// KoanfFile implements a KoanfFile provider. +type KoanfFile struct { + subKey string + path string + parser koanf.Parser +} + +// NewKoanfFile returns a file provider. +func NewKoanfFile(path string) (*KoanfFile, error) { + return NewKoanfFileSubKey(path, "") +} + +func NewKoanfFileSubKey(path, subKey string) (*KoanfFile, error) { + kf := &KoanfFile{ + path: filepath.Clean(path), + subKey: subKey, + } + + switch e := filepath.Ext(path); e { + case ".toml": + kf.parser = toml.Parser() + case ".json": + kf.parser = json.Parser() + case ".yaml", ".yml": + kf.parser = yaml.Parser() + default: + return nil, errors.Errorf("unknown config file extension: %s", e) + } + + return kf, nil +} + +// ReadBytes is not supported by KoanfFile. +func (f *KoanfFile) ReadBytes() ([]byte, error) { + return nil, errors.New("file provider does not support this method") +} + +// Read reads the file and returns the parsed configuration. +func (f *KoanfFile) Read() (map[string]interface{}, error) { + //#nosec G304 -- false positive + fc, err := os.ReadFile(f.path) + if err != nil { + return nil, errors.WithStack(err) + } + + v, err := f.parser.Unmarshal(fc) + if err != nil { + return nil, errors.WithStack(err) + } + + if f.subKey == "" { + return v, nil + } + + path := strings.Split(f.subKey, Delimiter) + for i := range path { + v = map[string]interface{}{ + path[len(path)-1-i]: v, + } + } + + return v, nil +} + +// WatchChannel watches the file and triggers a callback when it changes. It is a +// blocking function that internally spawns a goroutine to watch for changes. +func (f *KoanfFile) WatchChannel(ctx context.Context, c watcherx.EventChannel) (watcherx.Watcher, error) { + return watcherx.WatchFile(ctx, f.path, c) +} diff --git a/oryx/configx/koanf_full_merge.go b/oryx/configx/koanf_full_merge.go new file mode 100644 index 0000000..dc25868 --- /dev/null +++ b/oryx/configx/koanf_full_merge.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/tidwall/sjson" + + "github.com/ory/x/jsonx" +) + +func MergeAllTypes(src, dst map[string]interface{}) error { + rawSrc, err := json.Marshal(src) + if err != nil { + return errors.WithStack(err) + } + + dstSrc, err := json.Marshal(dst) + if err != nil { + return errors.WithStack(err) + } + + keys := jsonx.Flatten(rawSrc) + for key, value := range keys { + dstSrc, err = sjson.SetBytes(dstSrc, key, value) + if err != nil { + return errors.WithStack(err) + } + } + + return errors.WithStack(json.Unmarshal(dstSrc, &dst)) +} diff --git a/oryx/configx/koanf_memory.go b/oryx/configx/koanf_memory.go new file mode 100644 index 0000000..2eff963 --- /dev/null +++ b/oryx/configx/koanf_memory.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/v2" + + "github.com/pkg/errors" + + stdjson "encoding/json" +) + +// KoanfMemory implements a KoanfMemory provider. +type KoanfMemory struct { + doc stdjson.RawMessage + parser koanf.Parser +} + +// NewKoanfMemory returns a file provider. +func NewKoanfMemory(doc stdjson.RawMessage) *KoanfMemory { + return &KoanfMemory{ + doc: doc, + parser: json.Parser(), + } +} + +func (f *KoanfMemory) SetDoc(doc stdjson.RawMessage) { + f.doc = doc +} + +// ReadBytes reads the contents of a file on disk and returns the bytes. +func (f *KoanfMemory) ReadBytes() ([]byte, error) { + return nil, errors.New("file provider does not support this method") +} + +// Read is not supported by the file provider. +func (f *KoanfMemory) Read() (map[string]interface{}, error) { + v, err := f.parser.Unmarshal(f.doc) + if err != nil { + return nil, errors.WithStack(err) + } + + return v, nil +} diff --git a/oryx/configx/koanf_schema_defaults.go b/oryx/configx/koanf_schema_defaults.go new file mode 100644 index 0000000..6ed6607 --- /dev/null +++ b/oryx/configx/koanf_schema_defaults.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "strings" + + "github.com/knadh/koanf/maps" + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +type KoanfSchemaDefaults struct { + keys []jsonschemax.Path +} + +func NewKoanfSchemaDefaults(rawSchema []byte, schema *jsonschema.Schema) (*KoanfSchemaDefaults, error) { + keys, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + + return &KoanfSchemaDefaults{keys: keys}, nil +} + +func (k *KoanfSchemaDefaults) ReadBytes() ([]byte, error) { + return nil, errors.New("schema defaults provider does not support this method") +} + +func (k *KoanfSchemaDefaults) Read() (map[string]interface{}, error) { + values := map[string]interface{}{} + for _, key := range k.keys { + // It's an array! + if strings.Contains(key.Name, "#") { + continue + } + + if key.Default != nil { + values[key.Name] = key.Default + } + } + + // Deep-copy: getSchemaPaths caches Path entries by schema hash, so + // key.Default may be a map/slice shared across all callers. koanf.Merge + // retains references when merging, and recursively writes into shared + // sub-maps when a later provider overlaps the default's path — + // without this copy, concurrent configx.New calls race on the shared + // default value. + return maps.Unflatten(maps.Copy(values), "."), nil +} diff --git a/oryx/configx/options.go b/oryx/configx/options.go new file mode 100644 index 0000000..6a51797 --- /dev/null +++ b/oryx/configx/options.go @@ -0,0 +1,159 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/logrusx" + + "github.com/knadh/koanf/v2" + + "github.com/ory/x/watcherx" +) + +type ( + OptionModifier func(p *Provider) +) + +func WithContext(ctx context.Context) OptionModifier { + return func(p *Provider) { + for _, o := range ConfigOptionsFromContext(ctx) { + o(p) + } + } +} + +func WithConfigFiles(files ...string) OptionModifier { + return func(p *Provider) { + p.files = append(p.files, files...) + } +} + +func WithImmutables(immutables ...string) OptionModifier { + return func(p *Provider) { + p.immutables = append(p.immutables, immutables...) + } +} + +func WithExceptImmutables(exceptImmutables ...string) OptionModifier { + return func(p *Provider) { + p.exceptImmutables = append(p.exceptImmutables, exceptImmutables...) + } +} + +func WithFlags(flags *pflag.FlagSet) OptionModifier { + return func(p *Provider) { + p.flags = flags + } +} + +func WithLogger(l *logrusx.Logger) OptionModifier { + return func(p *Provider) { + p.logger = l + } +} + +func SkipValidation() OptionModifier { + return func(p *Provider) { + p.skipValidation = true + } +} + +func DisableEnvLoading() OptionModifier { + return func(p *Provider) { + p.disableEnvLoading = true + } +} + +func WithValue(key string, value interface{}) OptionModifier { + return func(p *Provider) { + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + } +} + +func WithValues(values map[string]interface{}) OptionModifier { + return func(p *Provider) { + for key, value := range values { + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + } + } +} + +func WithBaseValues(values map[string]interface{}) OptionModifier { + return func(p *Provider) { + for key, value := range values { + p.baseValues = append(p.baseValues, tuple{Key: key, Value: value}) + } + } +} + +func WithUserProviders(providers ...koanf.Provider) OptionModifier { + return func(p *Provider) { + p.userProviders = providers + } +} + +// DEPRECATED without replacement. This option is a no-op. +func OmitKeysFromTracing(keys ...string) OptionModifier { + return func(*Provider) {} +} + +func AttachWatcher(watcher func(event watcherx.Event, err error)) OptionModifier { + return func(p *Provider) { + p.onChanges = append(p.onChanges, watcher) + } +} + +func WithLogrusWatcher(l *logrusx.Logger) OptionModifier { + return AttachWatcher(LogrusWatcher(l)) +} + +func LogrusWatcher(l *logrusx.Logger) func(e watcherx.Event, err error) { + return func(e watcherx.Event, err error) { + l.WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("A change to a configuration file was detected.") + + if et := new(jsonschema.ValidationError); errors.As(err, &et) { + l.WithField("event", fmt.Sprintf("%#v", et)). + Errorf("The changed configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.") + } else if et := new(ImmutableError); errors.As(err, &et) { + l.WithError(err). + WithField("key", et.Key). + WithField("old_value", fmt.Sprintf("%v", et.From)). + WithField("new_value", fmt.Sprintf("%v", et.To)). + Errorf("A configuration value marked as immutable has changed. Rolling back to the last working configuration revision. To reload the values please restart the process.") + } else if err != nil { + l.WithError(err).Errorf("An error occurred while watching config file %s", e.Source()) + } else { + l.WithField("file", e.Source()). + WithField("event_type", fmt.Sprintf("%T", e)). + Info("Configuration change processed successfully.") + } + } +} + +func WithStderrValidationReporter() OptionModifier { + return func(p *Provider) { + p.onValidationError = func(k *koanf.Koanf, err error) { + p.printHumanReadableValidationErrors(k, os.Stderr, err) + } + } +} + +func WithStandardValidationReporter(w io.Writer) OptionModifier { + return func(p *Provider) { + p.onValidationError = func(k *koanf.Koanf, err error) { + p.printHumanReadableValidationErrors(k, w, err) + } + } +} diff --git a/oryx/configx/permission.go b/oryx/configx/permission.go new file mode 100644 index 0000000..51be0a5 --- /dev/null +++ b/oryx/configx/permission.go @@ -0,0 +1,56 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "os" + "os/user" + "strconv" +) + +type UnixPermission struct { + Owner string + Group string + Mode os.FileMode +} + +func (p *UnixPermission) SetPermission(file string) error { + var e error + e = os.Chmod(file, p.Mode) + if e != nil { + return e + } + + gid := -1 + uid := -1 + + if p.Owner != "" { + var userObj *user.User + userObj, e = user.Lookup(p.Owner) + if e != nil { + return e + } + uid, e = strconv.Atoi(userObj.Uid) + if e != nil { + return e + } + } + if p.Group != "" { + var group *user.Group + group, e := user.LookupGroup(p.Group) + if e != nil { + return e + } + gid, e = strconv.Atoi(group.Gid) + if e != nil { + return e + } + } + + e = os.Chown(file, uid, gid) + if e != nil { + return e + } + return nil +} diff --git a/oryx/configx/pflag.go b/oryx/configx/pflag.go new file mode 100644 index 0000000..9362a54 --- /dev/null +++ b/oryx/configx/pflag.go @@ -0,0 +1,57 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "strings" + + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/pkg/errors" + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +type PFlagProvider struct { + p *posflag.Posflag + paths []jsonschemax.Path +} + +func NewPFlagProvider(rawSchema []byte, schema *jsonschema.Schema, f *pflag.FlagSet, k *koanf.Koanf) (*PFlagProvider, error) { + paths, err := getSchemaPaths(rawSchema, schema) + if err != nil { + return nil, err + } + return &PFlagProvider{ + p: posflag.Provider(f, ".", k), + paths: paths, + }, nil +} + +func (p *PFlagProvider) ReadBytes() ([]byte, error) { + return nil, errors.New("pflag provider does not support this method") +} + +func (p *PFlagProvider) Read() (map[string]interface{}, error) { + all, err := p.p.Read() + if err != nil { + return nil, errors.WithStack(err) + } + knownFlags := make(map[string]interface{}, len(all)) + for k, v := range all { + k = strings.ReplaceAll(k, ".", "-") + for _, path := range p.paths { + normalized := strings.ReplaceAll(path.Name, ".", "-") + if k == normalized { + knownFlags[k] = v + break + } + } + } + return knownFlags, nil +} + +var _ koanf.Provider = (*PFlagProvider)(nil) diff --git a/oryx/configx/provider.go b/oryx/configx/provider.go new file mode 100644 index 0000000..efd71ed --- /dev/null +++ b/oryx/configx/provider.go @@ -0,0 +1,561 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "context" + "fmt" + "io" + "net/url" + "os" + "reflect" + "sync" + "time" + + "github.com/inhies/go-bytesize" + "github.com/knadh/koanf/parsers/json" + "github.com/knadh/koanf/providers/posflag" + "github.com/knadh/koanf/v2" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/spf13/pflag" + + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/watcherx" +) + +type tuple struct { + Key string + Value interface{} +} + +type Provider struct { + l sync.RWMutex + *koanf.Koanf + immutables, exceptImmutables []string + + schema []byte + flags *pflag.FlagSet + validator *jsonschema.Schema + onChanges []func(watcherx.Event, error) + onValidationError func(k *koanf.Koanf, err error) + + forcedValues []tuple + baseValues []tuple + files []string + + skipValidation bool + disableEnvLoading bool + + logger *logrusx.Logger + + providers []koanf.Provider + userProviders []koanf.Provider +} + +const ( + FlagConfig = "config" + Delimiter = "." +) + +// RegisterConfigFlag registers the "--config" flag on pflag.FlagSet. +func RegisterConfigFlag(flags *pflag.FlagSet, fallback []string) { + flags.StringSliceP(FlagConfig, "c", fallback, "Config files to load, overwriting in the order specified.") +} + +// New creates a new provider instance or errors. +// Configuration values are loaded in the following order: +// +// 1. Defaults from the JSON Schema +// 2. Config files (yaml, yml, toml, json) +// 3. Command line flags +// 4. Environment variables +// +// There will also be file-watchers started for all config files. To cancel the +// watchers, cancel the context. +func New(ctx context.Context, schema []byte, modifiers ...OptionModifier) (*Provider, error) { + validator, err := getSchema(ctx, schema) + if err != nil { + return nil, err + } + + l := logrus.New() + l.Out = io.Discard + + p := &Provider{ + schema: schema, + validator: validator, + onValidationError: func(k *koanf.Koanf, err error) {}, + logger: logrusx.New("discarding config logger", "", logrusx.UseLogger(l)), + Koanf: koanf.NewWithConf(koanf.Conf{Delim: Delimiter, StrictMerge: true}), + } + + for _, m := range modifiers { + m(p) + } + + providers, err := p.createProviders(ctx) + if err != nil { + return nil, err + } + + p.providers = providers + + k, err := p.newKoanf() + if err != nil { + return nil, err + } + + p.replaceKoanf(k) + return p, nil +} + +func (p *Provider) SkipValidation() bool { + return p.skipValidation +} + +func (p *Provider) createProviders(ctx context.Context) (providers []koanf.Provider, err error) { + defaultsProvider, err := NewKoanfSchemaDefaults(p.schema, p.validator) + if err != nil { + return nil, err + } + providers = append(providers, defaultsProvider) + + // Workaround for https://github.com/knadh/koanf/pull/47 + for _, t := range p.baseValues { + providers = append(providers, NewKoanfConfmap([]tuple{t})) + } + + paths := p.files + if p.flags != nil { + p, _ := p.flags.GetStringSlice(FlagConfig) + paths = append(paths, p...) + } + + p.logger.WithField("files", paths).Debug("Adding config files.") + + c := make(watcherx.EventChannel) + + defer func() { + if err == nil && len(paths) > 0 { + go p.watchForFileChanges(ctx, c) + } + }() + for _, path := range paths { + fp, err := NewKoanfFile(path) + if err != nil { + return nil, err + } + + if _, err := fp.WatchChannel(ctx, c); err != nil { + return nil, err + } + + providers = append(providers, fp) + } + + providers = append(providers, p.userProviders...) + + if p.flags != nil { + pp, err := NewPFlagProvider(p.schema, p.validator, p.flags, p.Koanf) + if err != nil { + return nil, err + } + providers = append(providers, pp) + } + + if !p.disableEnvLoading { + envProvider, err := NewKoanfEnv("", p.schema, p.validator) + if err != nil { + return nil, err + } + providers = append(providers, envProvider) + } + + // Workaround for https://github.com/knadh/koanf/pull/47 + for _, t := range p.forcedValues { + providers = append(providers, NewKoanfConfmap([]tuple{t})) + } + + return providers, nil +} + +func (p *Provider) replaceKoanf(k *koanf.Koanf) { + p.Koanf = k +} + +func (p *Provider) validate(k *koanf.Koanf) error { + if p.skipValidation { + return nil + } + + out, err := k.Marshal(json.Parser()) + if err != nil { + return errors.WithStack(err) + } + if err := p.validator.Validate(bytes.NewReader(out)); err != nil { + p.onValidationError(k, err) + return err + } + + return nil +} + +// newKoanf creates a new koanf instance with all the updated config +// +// This is unfortunately required due to several limitations / bugs in koanf: +// +// - https://github.com/knadh/koanf/issues/77 +// - https://github.com/knadh/koanf/pull/47 +func (p *Provider) newKoanf() (_ *koanf.Koanf, err error) { + k := koanf.New(Delimiter) + + for _, provider := range p.providers { + // posflag.Posflag requires access to Koanf instance so we recreate the provider here which is a workaround + // for posflag.Provider's API. + if _, ok := provider.(*posflag.Posflag); ok { + provider = posflag.Provider(p.flags, ".", k) + } + + var opts []koanf.Option + if _, ok := provider.(*Env); ok { + opts = append(opts, koanf.WithMergeFunc(MergeAllTypes)) + } + + if err := k.Load(provider, nil, opts...); err != nil { + return nil, err + } + } + + if err := p.validate(k); err != nil { + return nil, err + } + + return k, nil +} + +// SetTracer does nothing. DEPRECATED without replacement. +func (p *Provider) SetTracer(_ context.Context, _ *otelx.Tracer) { +} + +func (p *Provider) runOnChanges(e watcherx.Event, err error) { + for k := range p.onChanges { + p.onChanges[k](e, err) + } +} + +func deleteOtherKeys(k *koanf.Koanf, keys []string) { +outer: + for _, key := range k.Keys() { + for _, ik := range keys { + if key == ik { + continue outer + } + } + k.Delete(key) + } +} + +func (p *Provider) reload(e watcherx.Event) { + p.l.Lock() + + var err error + defer func() { + // we first want to unlock and then runOnChanges, so that the callbacks can actually use the Provider + p.l.Unlock() + p.runOnChanges(e, err) + }() + + nk, err := p.newKoanf() + if err != nil { + return // unlocks & runs changes in defer + } + + oldImmutables, newImmutables := p.Koanf.Copy(), nk.Copy() + deleteOtherKeys(oldImmutables, p.immutables) + deleteOtherKeys(newImmutables, p.immutables) + + for _, key := range p.exceptImmutables { + oldImmutables.Delete(key) + newImmutables.Delete(key) + } + if !reflect.DeepEqual(oldImmutables.Raw(), newImmutables.Raw()) { + for _, key := range p.immutables { + if !reflect.DeepEqual(oldImmutables.Get(key), newImmutables.Get(key)) { + err = NewImmutableError(key, fmt.Sprintf("%v", p.Koanf.Get(key)), fmt.Sprintf("%v", nk.Get(key))) + return // unlocks & runs changes in defer + } + } + } + + p.replaceKoanf(nk) + + // unlocks & runs changes in defer +} + +func (p *Provider) watchForFileChanges(ctx context.Context, c watcherx.EventChannel) { + for { + select { + case <-ctx.Done(): + return + case e := <-c: + switch et := e.(type) { + case *watcherx.ErrorEvent: + p.runOnChanges(e, et) + default: + p.reload(e) + } + } + } +} + +// DirtyPatch patches individual config keys without reloading the full config +// +// WARNING! This method is only useful to override existing keys in string or number +// format. DO NOT use this method to override arrays, maps, or other complex types. +// +// This method DOES NOT validate the config against the config JSON schema. If you +// need to validate the config, use the Set method instead. +// +// This method can not be used to remove keys from the config as that is not +// possible without reloading the full config. +func (p *Provider) DirtyPatch(key string, value any) error { + p.l.Lock() + defer p.l.Unlock() + + t := tuple{Key: key, Value: value} + kc := NewKoanfConfmap([]tuple{t}) + + p.forcedValues = append(p.forcedValues, t) + p.providers = append(p.providers, kc) + + if err := p.Koanf.Load(kc, nil, []koanf.Option{}...); err != nil { + return err + } + + return nil +} + +func (p *Provider) Set(key string, value interface{}) error { + p.l.Lock() + defer p.l.Unlock() + + p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value}) + p.providers = append(p.providers, NewKoanfConfmap([]tuple{{Key: key, Value: value}})) + + k, err := p.newKoanf() + if err != nil { + return err + } + + p.replaceKoanf(k) + return nil +} + +// ConfigFiles returns all config file paths configured on this provider, +// including paths set via WithConfigFiles and via the --config flag. +func (p *Provider) ConfigFiles() []string { + paths := make([]string, len(p.files)) + copy(paths, p.files) + if p.flags != nil { + flagPaths, _ := p.flags.GetStringSlice(FlagConfig) + paths = append(paths, flagPaths...) + } + return paths +} + +func (p *Provider) BoolF(key string, fallback bool) bool { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Bool(key) +} + +func (p *Provider) StringF(key string, fallback string) string { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.String(key) +} + +func (p *Provider) StringsF(key string, fallback []string) (val []string) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Strings(key) +} + +func (p *Provider) IntF(key string, fallback int) (val int) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Int(key) +} + +func (p *Provider) Float64F(key string, fallback float64) (val float64) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Float64(key) +} + +func (p *Provider) DurationF(key string, fallback time.Duration) (val time.Duration) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + return p.Duration(key) +} + +func (p *Provider) ByteSizeF(key string, fallback bytesize.ByteSize) bytesize.ByteSize { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Koanf.Exists(key) { + return fallback + } + + switch v := p.Koanf.Get(key).(type) { + case string: + // this type usually comes from user input + dec, err := bytesize.Parse(v) + if err != nil { + p.logger.WithField("key", key).WithField("raw_value", v).WithError(err).Warnf("error parsing byte size value, using fallback of %s", fallback) + return fallback + } + return dec + case float64: + // this type comes from json.Unmarshal + return bytesize.ByteSize(v) + case bytesize.ByteSize: + return v + default: + p.logger.WithField("key", key).WithField("raw_type", fmt.Sprintf("%T", v)).WithField("raw_value", fmt.Sprintf("%+v", v)).Errorf("error converting byte size value because of unknown type, using fallback of %s", fallback) + return fallback + } +} + +func (p *Provider) GetF(key string, fallback interface{}) (val interface{}) { + p.l.RLock() + defer p.l.RUnlock() + + if !p.Exists(key) { + return fallback + } + + return p.Get(key) +} + +func (p *Provider) TracingConfig(serviceName string) *otelx.Config { + return &otelx.Config{ + ServiceName: p.StringF("tracing.service_name", serviceName), + DeploymentEnvironment: p.StringF("tracing.deployment_environment", ""), + Provider: p.String("tracing.provider"), + Providers: otelx.ProvidersConfig{ + Jaeger: otelx.JaegerConfig{ + Sampling: otelx.JaegerSampling{ + ServerURL: p.String("tracing.providers.jaeger.sampling.server_url"), + TraceIDRatio: p.Float64F("tracing.providers.jaeger.sampling.trace_id_ratio", 1), + }, + LocalAgentAddress: p.String("tracing.providers.jaeger.local_agent_address"), + }, + Zipkin: otelx.ZipkinConfig{ + ServerURL: p.String("tracing.providers.zipkin.server_url"), + Sampling: otelx.ZipkinSampling{ + SamplingRatio: p.Float64("tracing.providers.zipkin.sampling.sampling_ratio"), + }, + }, + OTLP: otelx.OTLPConfig{ + ServerURL: p.String("tracing.providers.otlp.server_url"), + Insecure: p.Bool("tracing.providers.otlp.insecure"), + Sampling: otelx.OTLPSampling{ + SamplingRatio: p.Float64F("tracing.providers.otlp.sampling.sampling_ratio", 1), + }, + AuthorizationHeader: p.String("tracing.providers.otlp.authorization_header"), + }, + }, + } +} + +func (p *Provider) RequestURIF(path string, fallback *url.URL) *url.URL { + p.l.RLock() + defer p.l.RUnlock() + + switch t := p.Get(path).(type) { + case *url.URL: + return t + case url.URL: + return &t + case string: + if parsed, err := url.ParseRequestURI(t); err == nil { + return parsed + } + } + + return fallback +} + +func (p *Provider) URIF(path string, fallback *url.URL) *url.URL { + p.l.RLock() + defer p.l.RUnlock() + + switch t := p.Get(path).(type) { + case *url.URL: + return t + case url.URL: + return &t + case string: + if parsed, err := url.Parse(t); err == nil { + return parsed + } + } + + return fallback +} + +// PrintHumanReadableValidationErrors prints human readable validation errors. Duh. +func (p *Provider) PrintHumanReadableValidationErrors(w io.Writer, err error) { + p.printHumanReadableValidationErrors(p.Koanf, w, err) +} + +func (p *Provider) printHumanReadableValidationErrors(k *koanf.Koanf, w io.Writer, err error) { + if err == nil { + return + } + + _, _ = fmt.Fprintln(os.Stderr, "") + conf, innerErr := k.Marshal(json.Parser()) + if innerErr != nil { + _, _ = fmt.Fprintf(w, "Unable to unmarshal configuration: %+v", innerErr) + } + + jsonschemax.FormatValidationErrorForCLI(w, conf, err) +} diff --git a/oryx/configx/schema.go b/oryx/configx/schema.go new file mode 100644 index 0000000..9ee1381 --- /dev/null +++ b/oryx/configx/schema.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "bytes" + "fmt" + + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" +) + +func newCompiler(schema []byte) (string, *jsonschema.Compiler, error) { + id := gjson.GetBytes(schema, "$id").String() + if id == "" { + id = fmt.Sprintf("%s.json", uuid.Must(uuid.NewV4()).String()) + } + + compiler := jsonschema.NewCompiler() + if err := compiler.AddResource(id, bytes.NewReader(schema)); err != nil { + return "", nil, errors.WithStack(err) + } + + // DO NOT REMOVE THIS + compiler.ExtractAnnotations = true + + if err := otelx.AddConfigSchema(compiler); err != nil { + return "", nil, err + } + if err := logrusx.AddConfigSchema(compiler); err != nil { + return "", nil, err + } + if err := AddSchemaResources(compiler); err != nil { + return "", nil, err + } + + return id, compiler, nil +} diff --git a/oryx/configx/schema_cache.go b/oryx/configx/schema_cache.go new file mode 100644 index 0000000..023fb18 --- /dev/null +++ b/oryx/configx/schema_cache.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "context" + "crypto/sha256" + + "github.com/dgraph-io/ristretto/v2" + + "github.com/ory/jsonschema/v3" +) + +var schemaCacheConfig = &ristretto.Config[[]byte, *jsonschema.Schema]{ + // Hold up to 25 schemas in cache. Usually we only need one. + MaxCost: 25, + NumCounters: 250, + BufferItems: 64, + Metrics: false, + IgnoreInternalCost: true, + Cost: func(*jsonschema.Schema) int64 { + return 1 + }, +} + +var schemaCache, _ = ristretto.NewCache(schemaCacheConfig) + +func getSchema(ctx context.Context, schema []byte) (*jsonschema.Schema, error) { + key := sha256.Sum256(schema) + if val, found := schemaCache.Get(key[:]); found { + return val, nil + } + + schemaID, comp, err := newCompiler(schema) + if err != nil { + return nil, err + } + + validator, err := comp.Compile(ctx, schemaID) + if err != nil { + return nil, err + } + + schemaCache.Set(key[:], validator, 1) + schemaCache.Wait() + return validator, nil +} diff --git a/oryx/configx/schema_path_cache.go b/oryx/configx/schema_path_cache.go new file mode 100644 index 0000000..3b3d5f5 --- /dev/null +++ b/oryx/configx/schema_path_cache.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "crypto/sha256" + + "github.com/ory/x/jsonschemax" + + "github.com/dgraph-io/ristretto/v2" + + "github.com/ory/jsonschema/v3" +) + +var schemaPathCacheConfig = &ristretto.Config[[]byte, []jsonschemax.Path]{ + // Hold up to 25 schemas in cache. Usually we only need one. + MaxCost: 250, + NumCounters: 2500, + BufferItems: 64, + Metrics: false, + IgnoreInternalCost: true, +} + +var schemaPathCache, _ = ristretto.NewCache(schemaPathCacheConfig) + +func getSchemaPaths(rawSchema []byte, schema *jsonschema.Schema) ([]jsonschemax.Path, error) { + key := sha256.Sum256(rawSchema) + if val, found := schemaPathCache.Get(key[:]); found { + return val, nil + } + + keys, err := jsonschemax.ListPathsWithInitializedSchemaAndArraysIncluded(schema) + if err != nil { + return nil, err + } + + schemaPathCache.Set(key[:], keys, 1) + schemaPathCache.Wait() + return keys, nil +} diff --git a/oryx/configx/serve.go b/oryx/configx/serve.go new file mode 100644 index 0000000..0249ccf --- /dev/null +++ b/oryx/configx/serve.go @@ -0,0 +1,137 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +import ( + "cmp" + "context" + "crypto/tls" + _ "embed" + "fmt" + "net/url" + "os" + + "github.com/ory/x/logrusx" + "github.com/ory/x/tlsx" +) + +const ( + ServeConfigSchemaID = "ory://serve-config" + TLSConfigSchemaID = "ory://tls-config" +) + +//go:embed serve.schema.json +var ServeConfigSchema []byte + +//go:embed tls.schema.json +var TLSConfigSchema []byte + +type ( + Serve struct { + Host, WriteListenFile string + Port int + BaseURL *url.URL + Socket UnixPermission + TLS TLS + RequestLog ServeRequestLog + } + TLS struct { + Enabled bool + AllowTerminationFrom []string + CertBase64, KeyBase64, CertPath, KeyPath string + } + ServeRequestLog struct { + DisableHealth bool + } +) + +func (p *Provider) Serve(prefix string, isDev bool, defaults Serve) *Serve { + prefix = cleanPrefix(prefix) + + defaults.Socket.Mode = cmp.Or(defaults.Socket.Mode, 0o755) + + serve := Serve{ + Host: p.StringF(prefix+"host", defaults.Host), + Port: p.IntF(prefix+"port", defaults.Port), + WriteListenFile: p.StringF(prefix+"write_listen_file", defaults.WriteListenFile), + BaseURL: p.URIF(prefix+"base_url", defaults.BaseURL), + Socket: UnixPermission{ + Owner: p.StringF(prefix+"socket.owner", defaults.Socket.Owner), + Group: p.StringF(prefix+"socket.group", defaults.Socket.Group), + Mode: os.FileMode(p.IntF(prefix+"socket.mode", int(defaults.Socket.Mode))), + }, + TLS: p.TLS(prefix+"tls", defaults.TLS), + RequestLog: ServeRequestLog{ + DisableHealth: p.BoolF(prefix+"request_log.disable_for_health", defaults.RequestLog.DisableHealth), + }, + } + + if serve.BaseURL == nil { + serve.BaseURL = &url.URL{ + Scheme: "http", + Path: "/", + } + if !isDev || serve.TLS.Enabled { + serve.BaseURL.Scheme = "https" + } + host := serve.Host + if host == "0.0.0.0" || host == "" { + var err error + host, err = os.Hostname() + if err != nil { + p.logger.WithError(err).Warn("Unable to get hostname from system, falling back to 127.0.0.1.") + host = "127.0.0.1" + } + } + serve.BaseURL.Host = fmt.Sprintf("%s:%d", host, serve.Port) + } + + return &serve +} + +func (p *Provider) TLS(prefix string, defaults TLS) TLS { + prefix = cleanPrefix(prefix) + + return TLS{ + Enabled: p.BoolF(prefix+"enabled", defaults.Enabled), + AllowTerminationFrom: p.StringsF(prefix+"allow_termination_from", defaults.AllowTerminationFrom), + CertBase64: p.StringF(prefix+"cert.base64", defaults.CertBase64), + KeyBase64: p.StringF(prefix+"key.base64", defaults.KeyBase64), + CertPath: p.StringF(prefix+"cert.path", defaults.CertPath), + KeyPath: p.StringF(prefix+"key.path", defaults.KeyPath), + } +} + +func (t *TLS) GetCertFunc(ctx context.Context, l *logrusx.Logger, ifaceName string) (tlsx.CertFunc, error) { + switch { + case t.CertBase64 != "" && t.KeyBase64 != "": + cert, err := tlsx.CertificateFromBase64(t.CertBase64, t.KeyBase64) + if err != nil { + return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err) + } + l.Infof("Setting up HTTPS for %s", ifaceName) + return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return &cert, nil }, nil + case t.CertPath != "" && t.KeyPath != "": + errs := make(chan error, 1) + getCert, err := tlsx.GetCertificate(ctx, t.CertPath, t.KeyPath, errs) + if err != nil { + return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err) + } + go func() { + for { + select { + case <-ctx.Done(): + return + case err := <-errs: + l.WithError(err).Error("Failed to reload TLS certificates, using previous certificates") + } + } + }() + l.Infof("Setting up HTTPS for %s (automatic certificate reloading active)", ifaceName) + return getCert, nil + default: + l.Infof("TLS has not been configured for %s, skipping", ifaceName) + } + return nil, nil +} diff --git a/oryx/configx/serve.schema.json b/oryx/configx/serve.schema.json new file mode 100644 index 0000000..1f1df38 --- /dev/null +++ b/oryx/configx/serve.schema.json @@ -0,0 +1,70 @@ +{ + "$id": "https://github.com/ory/x/configx/serve.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "request_log": { + "type": "object", + "properties": { + "disable_for_health": { + "title": "Disable health endpoints request logging", + "description": "Disable request logging for /health/alive and /health/ready endpoints", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + }, + "base_url": { + "title": "Base URL", + "description": "The URL where the endpoint is exposed at. This domain is used to generate redirects, form URLs, and more.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/", + "https://my-app.com/.ory/kratos/public", + "https://auth.my-app.com/hydra" + ] + }, + "host": { + "title": "Host", + "description": "The host (interface) that the endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Port", + "description": "The port that the endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535 + }, + "socket": { + "type": "object", + "additionalProperties": false, + "description": "Sets the permissions of the unix socket", + "properties": { + "owner": { + "type": "string", + "description": "Owner of unix socket. If empty, the owner will be the user running the service.", + "default": "" + }, + "group": { + "type": "string", + "description": "Group of unix socket. If empty, the group will be the primary group of the user running the service.", + "default": "" + }, + "mode": { + "type": "integer", + "description": "Mode of unix socket in numeric form", + "default": 493, + "minimum": 0, + "maximum": 511 + } + } + }, + "tls": { + "$ref": "ory://tls-config" + } + } +} diff --git a/oryx/configx/span.go b/oryx/configx/span.go new file mode 100644 index 0000000..2f2471e --- /dev/null +++ b/oryx/configx/span.go @@ -0,0 +1,10 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package configx + +const ( + LoadSpanOpName = "config-load" + UpdatedSpanOpName = "config-update" + SnapshotSpanOpName = "config-snapshot" +) diff --git a/oryx/configx/stub/benchmark/benchmark.yaml b/oryx/configx/stub/benchmark/benchmark.yaml new file mode 100644 index 0000000..4723b37 --- /dev/null +++ b/oryx/configx/stub/benchmark/benchmark.yaml @@ -0,0 +1,312 @@ +# Please find the documentation for this file at +# https://www.ory.com/oathkeeper/docs/configuration + +log: + level: debug + format: json + +profiling: cpu + +serve: + proxy: + port: 1234 + host: 127.0.0.1 + + timeout: + read: 1s + write: 2s + idle: 3s + + cors: + enabled: true + allowed_origins: + - https://example.com + - https://*.example.com + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + max_age: 10 + debug: true + tls: + key: + path: /path/to/key.pem + base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3... + cert: + path: /path/to/cert.pem + base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr... + + api: + port: 1235 + host: 127.0.0.2 + + timeout: + read: 1s + write: 2s + idle: 3s + + cors: + enabled: true + allowed_origins: + - https://example.org + - https://*.example.org + allowed_methods: + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Content-Type + exposed_headers: + - Content-Type + allow_credentials: true + max_age: 10 + debug: true + tls: + key: + path: /path/to/key.pem + base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3... + cert: + path: /path/to/cert.pem + base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr... + + prometheus: + port: 9000 + host: localhost + metrics_path: /metrics + collapse_request_paths: true + +# Configures Access Rules +access_rules: + # Locations (list of URLs) where access rules should be fetched from on boot. + # It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules. + repositories: + # If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be + # fetched from the local file system. + - file://path/to/rules.json + # If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) + # are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{"id":"foo-rule","authenticators":[....]}]`)): + - inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d + # If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be + # fetched from the provided HTTP(s) location. + - https://path-to-my-rules/rules.json + # Optional fields describing matching strategy, defaults to "regexp". + matching_strategy: glob + +errors: + fallback: + - json + handlers: + redirect: + enabled: true + config: + to: http://path-to/redirect + json: + enabled: true + config: + verbose: true + when: + - error: + - unauthorized + - forbidden + - internal_server_error + request: + header: + content_type: + - application/json + accept: + - application/json + cidr: + - 127.0.0.0/24 + +# All authenticators can be configured under this configuration key +authenticators: + # Configures the anonymous authenticator + anonymous: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # Sets the anonymous username. Defaults to "anonymous". Common names include "guest", "anon", "anonymous", "unknown". + subject: guest + + # Configures the cookie session authenticator + cookie_session: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # Sets the origin to proxy requests to. If the response is a 200 with body `{ "subject": "...", "extra": {} }` + # The request will pass the subject through successfully, otherwise it will be marked as unauthorized + check_session_url: https://session-store-host + + # Sets a list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if + # none of the passed cookies are set on the request + only: + - sessionid + + # Configures the jwt authenticator + jwt: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The URL where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web + # Token. Usually something like "https://my-keys.com/.well-known/jwks.json". The response of that endpoint must + # return a JSON Web Key Set (JWKS). + jwks_urls: + - https://my-website.com/.well-known/jwks.json + - https://my-other-website.com/.well-known/jwks.json + - file://path/to/local/jwks.json + + # Sets the strategy to be used to validate/match the scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults + # to "none". + scope_strategy: wildcard + + # Configures the noop authenticator + noop: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + # Configures the oauth2_client_credentials authenticator + oauth2_client_credentials: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint that will be used to validate the client credentials. + token_url: https://my-website.com/oauth2/token + + # Configures the oauth2_introspection authenticator + oauth2_introspection: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The OAuth 2.0 Token Introspection endpoint. + introspection_url: https://my-website.com/oauth2/introspection + + # Sets the strategy to be used to validate/match the token scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults + # to "none". + scope_strategy: exact + + # Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer + # Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant. + pre_authorization: + # Enable pre-authorization. Defaults to false. + enabled: true + + # REQUIRED IF ENABLED - The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant. + client_id: some_id + + # REQUIRED IF ENABLED - The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant. + client_secret: some_secret + + # The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant. + scope: + - foo + - bar + + # REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed. + token_url: https://my-website.com/oauth2/token + + # Configures the unauthorized authenticator + unauthorized: + # Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false. + enabled: true + +# All authorizers can be configured under this configuration key +authorizers: + # Configures the allow authorizer + allow: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + # Configures the deny authorizer + deny: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + # Configures the keto_engine_acp_ory authorizer + keto_engine_acp_ory: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + # REQUIRED IF ENABLED - The base URL of ORY Keto, typically something like http(s)://[:]/ + base_url: http://my-keto/ + required_action: unknown + required_resource: unknown + + # Configures the remote authorizer + remote: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + remote: https://host/path + headers: {} + + # Configures the remote_json authorizer + remote_json: + # Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false. + enabled: true + + config: + remote: https://host/path + payload: "{}" + +# All mutators can be configured under this configuration key +mutators: + header: + enabled: true + config: + headers: + foo: bar + + # Configures the cookie mutator + cookie: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + config: + cookies: + foo: bar + + # Configures the hydrator mutator + hydrator: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + + config: + api: + url: https://some-url/ + + # Configures the id_token mutator + id_token: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true + config: + # REQUIRED IF ENABLED - Sets the "iss" value of the ID Token. + issuer_url: https://my-oathkeeper/ + # REQUIRED IF ENABLED - Sets the URL where keys should be fetched from. Supports remote locations (http, https) as + # well as local filesystem paths. + jwks_url: https://fetch-keys/from/this/location.json + # jwks_url: file:///from/this/absolute/location.json + # jwks_url: file://../from/this/relative/location.json + + # Sets the time-to-live of the ID token. Defaults to one minute. Valid time units are: s (second), m (minute), h (hour). + ttl: 1h + + # Configures the noop mutator + noop: + # Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false. + enabled: true diff --git a/oryx/configx/stub/benchmark/schema.config.json b/oryx/configx/stub/benchmark/schema.config.json new file mode 100644 index 0000000..08d9faf --- /dev/null +++ b/oryx/configx/stub/benchmark/schema.config.json @@ -0,0 +1,204 @@ +{ + "log": { + "level": "debug", + "format": "json" + }, + "profiling": "cpu", + "serve": { + "proxy": { + "port": 1234, + "host": "127.0.0.1", + "timeout": { + "read": "1s", + "write": "2s", + "idle": "3s" + }, + "cors": { + "enabled": true, + "allowed_origins": ["https://example.com", "https://*.example.com"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_headers": ["Authorization", "Content-Type"], + "exposed_headers": ["Content-Type"], + "allow_credentials": true, + "max_age": 10, + "debug": true + }, + "tls": { + "key": { + "path": "/path/to/key.pem", + "base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..." + }, + "cert": { + "path": "/path/to/cert.pem", + "base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + } + } + }, + "api": { + "port": 1235, + "host": "127.0.0.2", + "cors": { + "enabled": true, + "allowed_origins": ["https://example.org", "https://*.example.org"], + "allowed_methods": ["GET", "PUT", "PATCH", "DELETE"], + "allowed_headers": ["Authorization", "Content-Type"], + "exposed_headers": ["Content-Type"], + "allow_credentials": true, + "max_age": 10, + "debug": true + }, + "tls": { + "key": { + "path": "/path/to/key.pem", + "base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..." + }, + "cert": { + "path": "/path/to/cert.pem", + "base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + } + } + } + }, + "access_rules": { + "repositories": [ + "file://path/to/rules.json", + "inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d", + "https://path-to-my-rules/rules.json" + ], + "matching_strategy": "glob" + }, + "errors": { + "fallback": ["json"], + "handlers": { + "redirect": { + "enabled": true, + "config": { + "to": "http://path-to/redirect" + } + }, + "json": { + "enabled": true, + "config": { + "verbose": true, + "when": [ + { + "error": ["unauthorized", "forbidden", "internal_server_error"], + "request": { + "header": { + "content_type": ["application/json"], + "accept": ["application/json"] + }, + "cidr": ["127.0.0.0/24"] + } + } + ] + } + } + } + }, + "authenticators": { + "anonymous": { + "enabled": true, + "config": { + "subject": "guest" + } + }, + "cookie_session": { + "enabled": true, + "config": { + "check_session_url": "https://session-store-host", + "only": ["sessionid"] + } + }, + "jwt": { + "enabled": true, + "config": { + "jwks_urls": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ], + "scope_strategy": "wildcard" + } + }, + "noop": { + "enabled": true + }, + "oauth2_client_credentials": { + "enabled": true, + "config": { + "token_url": "https://my-website.com/oauth2/token" + } + }, + "oauth2_introspection": { + "enabled": true, + "config": { + "introspection_url": "https://my-website.com/oauth2/introspection", + "scope_strategy": "exact", + "pre_authorization": { + "enabled": true, + "client_id": "some_id", + "client_secret": "some_secret", + "scope": ["foo", "bar"], + "token_url": "https://my-website.com/oauth2/token" + } + } + }, + "unauthorized": { + "enabled": true + } + }, + "authorizers": { + "allow": { + "enabled": true + }, + "deny": { + "enabled": true + }, + "keto_engine_acp_ory": { + "enabled": true, + "config": { + "base_url": "http://my-keto/", + "required_action": "unknown", + "required_resource": "unknown" + } + } + }, + "mutators": { + "header": { + "enabled": false, + "config": { + "headers": { + "foo": "bar" + } + } + }, + "cookie": { + "enabled": true, + "config": { + "cookies": { + "foo": "bar" + } + } + }, + "hydrator": { + "enabled": true, + "config": { + "api": { + "url": "https://some-url/" + } + } + }, + "id_token": { + "enabled": true, + "config": { + "issuer_url": "https://my-oathkeeper/", + "jwks_url": "https://fetch-keys/from/this/location.json", + "ttl": "1h" + } + }, + "noop": { + "enabled": true + } + } +} diff --git a/oryx/configx/stub/domain-aliases/config.schema.json b/oryx/configx/stub/domain-aliases/config.schema.json new file mode 100644 index 0000000..7900791 --- /dev/null +++ b/oryx/configx/stub/domain-aliases/config.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "domain_aliases": { + "title": "Domain Aliases", + "description": "Adds an alias domain. If a request with the hostname (FQDN) matching the hostname in the alias is found, that URL is used as the base URL.", + "type": "array", + "items": [ + { + "additionalProperties": false, + "type": "object", + "required": ["match_domain", "base_path", "scheme"], + "properties": { + "match_domain": { + "minLength": 1, + "title": "Matching Domain", + "description": "Sets the matching domain. If the domain matches with this entry, the accompanying base_url will be used.", + "type": "string", + "examples": ["localhost", "my-domain.com"] + }, + "scheme": { + "title": "Scheme", + "description": "Sets the scheme, for example https or http.", + "type": "string", + "enum": ["http", "https"] + }, + "base_path": { + "minLength": 1, + "title": "Base Path", + "description": "Sets the base path for the matched domain.", + "type": "string", + "default": "/", + "pattern": "^/.*$", + "examples": ["/", "/.ory/kratos"] + } + } + } + ] + } + } +} diff --git a/oryx/configx/stub/from-files/a.yaml b/oryx/configx/stub/from-files/a.yaml new file mode 100644 index 0000000..f3e1808 --- /dev/null +++ b/oryx/configx/stub/from-files/a.yaml @@ -0,0 +1,27 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings diff --git a/oryx/configx/stub/from-files/b.yaml b/oryx/configx/stub/from-files/b.yaml new file mode 100644 index 0000000..1d48989 --- /dev/null +++ b/oryx/configx/stub/from-files/b.yaml @@ -0,0 +1,54 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/from-files/config.schema.json b/oryx/configx/stub/from-files/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/from-files/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/from-files/expected.json b/oryx/configx/stub/from-files/expected.json new file mode 100644 index 0000000..2fe5dd1 --- /dev/null +++ b/oryx/configx/stub/from-files/expected.json @@ -0,0 +1,124 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": false + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/hydra/config.schema.json b/oryx/configx/stub/hydra/config.schema.json new file mode 100644 index 0000000..e2ce4af --- /dev/null +++ b/oryx/configx/stub/hydra/config.schema.json @@ -0,0 +1,792 @@ +{ + "$id": "https://github.com/ory/hydra/docs/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Hydra Configuration", + "type": "object", + "definitions": { + "http_method": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + }, + "port_number": { + "type": "integer", + "description": "The port to listen on.", + "minimum": 1, + "maximum": 65535 + }, + "socket": { + "type": "object", + "additionalProperties": false, + "description": "Sets the permissions of the unix socket", + "properties": { + "owner": { + "type": "string", + "description": "Owner of unix socket. If empty, the owner will be the user running hydra.", + "default": "" + }, + "group": { + "type": "string", + "description": "Group of unix socket. If empty, the group will be the primary group of the user running hydra.", + "default": "" + }, + "mode": { + "type": "integer", + "description": "Mode of unix socket in numeric form", + "default": 493, + "minimum": 0, + "maximum": 511 + } + } + }, + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": ["Authorization", "Content-Type"], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "pem_file": { + "type": "object", + "oneOf": [ + { + "properties": { + "path": { + "type": "string", + "description": "The path to the pem file.", + "examples": ["/path/to/file.pem"] + } + }, + "additionalProperties": false, + "required": ["path"] + }, + { + "properties": { + "base64": { + "type": "string", + "description": "The base64 encoded string (without padding).", + "contentEncoding": "base64", + "contentMediaType": "application/x-pem-file", + "examples": ["b3J5IGh5ZHJhIGlzIGF3ZXNvbWUK"] + } + }, + "additionalProperties": false, + "required": ["base64"] + } + ] + }, + "duration": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "examples": ["1h"] + } + }, + "properties": { + "log": { + "type": "object", + "additionalProperties": false, + "description": "Configures the logger", + "properties": { + "level": { + "type": "string", + "description": "Sets the log level.", + "enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"], + "default": "info" + }, + "leak_sensitive_values": { + "type": "boolean", + "description": "Logs sensitive values such as cookie and URL parameter.", + "default": false + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "description": "Sets the log format.", + "enum": ["json", "json_pretty", "text"], + "default": "text" + } + } + }, + "serve": { + "type": "object", + "additionalProperties": false, + "description": "Controls the configuration for the http(s) daemon(s).", + "properties": { + "public": { + "type": "object", + "additionalProperties": false, + "description": "Controls the public daemon serving public API endpoints like /oauth2/auth, /oauth2/token, /.well-known/jwks.json", + "properties": { + "port": { + "default": 4444, + "allOf": [ + { + "$ref": "#/definitions/port_number" + } + ] + }, + "host": { + "type": "string", + "description": "The interface or unix socket ORY Hydra should listen and handle public API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.", + "default": "", + "examples": ["localhost"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "access_log": { + "type": "object", + "additionalProperties": false, + "description": "Access Log configuration for public server.", + "properties": { + "disable_for_health": { + "type": "boolean", + "description": "Disable access log for health endpoints.", + "default": false + } + } + } + } + }, + "admin": { + "type": "object", + "additionalProperties": false, + "properties": { + "port": { + "default": 4445, + "allOf": [ + { + "$ref": "#/definitions/port_number" + } + ] + }, + "host": { + "type": "string", + "description": "The interface or unix socket ORY Hydra should listen and handle administrative API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.", + "default": "", + "examples": ["localhost"] + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "socket": { + "$ref": "#/definitions/socket" + }, + "access_log": { + "type": "object", + "additionalProperties": false, + "description": "Access Log configuration for admin server.", + "properties": { + "disable_for_health": { + "type": "boolean", + "description": "Disable access log for health endpoints.", + "default": false + } + } + } + } + }, + "tls": { + "type": "object", + "additionalProperties": false, + "description": "Configures HTTPS (HTTP over TLS). If configured, the server automatically supports HTTP/2.", + "properties": { + "key": { + "description": "Configures the private key (pem encoded).", + "allOf": [ + { + "$ref": "#/definitions/pem_file" + } + ] + }, + "cert": { + "description": "Configures the private key (pem encoded).", + "allOf": [ + { + "$ref": "#/definitions/pem_file" + } + ] + }, + "allow_termination_from": { + "type": "array", + "description": "Whitelist one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. Hydra serves http instead of https when this option is set.", + "items": { + "type": "string", + "oneOf": [ + { + "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + { + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" + } + ], + "examples": ["127.0.0.1/32"] + } + } + } + }, + "cookies": { + "type": "object", + "additionalProperties": false, + "properties": { + "same_site_mode": { + "type": "string", + "description": "Specify the SameSite mode that cookies should be sent with.", + "enum": ["Strict", "Lax", "None"], + "default": "None" + }, + "same_site_legacy_workaround": { + "type": "boolean", + "description": "Some older browser versions don’t work with SameSite=None. This option enables the workaround defined in https://web.dev/samesite-cookie-recipes/ which essentially stores a second cookie without SameSite as a fallback.", + "default": false, + "examples": [true] + } + } + } + } + }, + "dsn": { + "type": "string", + "description": "Sets the data source name. This configures the backend where ORY Hydra persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.sh/docs/hydra/dependencies-environment#sql" + }, + "webfinger": { + "type": "object", + "additionalProperties": false, + "description": "Configures ./well-known/ settings.", + "properties": { + "jwks": { + "type": "object", + "additionalProperties": false, + "description": "Configures the /.well-known/jwks.json endpoint.", + "properties": { + "broadcast_keys": { + "type": "array", + "description": "A list of JSON Web Keys that should be exposed at that endpoint. This is usually the public key for verifying OpenID Connect ID Tokens. However, you might want to add additional keys here as well.", + "items": { + "type": "string" + }, + "default": ["hydra.openid.id-token"], + "examples": ["hydra.jwt.access-token"] + } + } + }, + "oidc_discovery": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect Discovery (/.well-known/openid-configuration).", + "properties": { + "jwks_url": { + "type": "string", + "description": "Overwrites the JWKS URL", + "format": "uri", + "examples": ["https://my-service.com/.well-known/jwks.json"] + }, + "token_url": { + "type": "string", + "description": "Overwrites the OAuth2 Token URL", + "format": "uri", + "examples": ["https://my-service.com/oauth2/token"] + }, + "auth_url": { + "type": "string", + "description": "Overwrites the OAuth2 Auth URL", + "format": "uri", + "examples": ["https://my-service.com/oauth2/auth"] + }, + "client_registration_url": { + "description": "Sets the OpenID Connect Dynamic Client Registration Endpoint", + "type": "string", + "format": "uri", + "examples": ["https://my-service.com/clients"] + }, + "supported_claims": { + "type": "array", + "description": "A list of supported claims to be broadcasted. Claim \"sub\" is always included.", + "items": { + "type": "string" + }, + "examples": [["email", "username"]] + }, + "supported_scope": { + "type": "array", + "description": "The scope OAuth 2.0 Clients may request. Scope `offline`, `offline_access`, and `openid` are always included.", + "items": { + "type": "string" + }, + "examples": [["email", "whatever", "read.photos"]] + }, + "userinfo_url": { + "type": "string", + "description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to ORY Hydra's userinfo endpoint at /userinfo. Set this value if you want to handle this endpoint yourself.", + "format": "uri", + "examples": ["https://example.org/my-custom-userinfo-endpoint"] + } + } + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect features.", + "properties": { + "subject_identifiers": { + "type": "object", + "additionalProperties": false, + "description": "Configures the Subject Identifier algorithm. For more information please head over to the documentation: https://www.ory.sh/docs/hydra/advanced#subject-identifier-algorithms", + "properties": { + "enabled": { + "type": "array", + "description": "A list of algorithms to enable.", + "items": { + "type": "string", + "enum": ["public", "pairwise"] + } + }, + "pairwise": { + "type": "object", + "additionalProperties": false, + "description": "Configures the pairwise algorithm.", + "properties": { + "salt": { + "type": "string" + } + }, + "required": ["salt"] + } + }, + "if": { + "properties": { + "enabled": { + "contains": { + "const": "pairwise" + } + } + } + }, + "then": { + "required": ["pairwise"] + }, + "else": { + "properties": { + "pairwise": { + "$comment": "This enforces pairwise to not be set if 'enabled' does not contain 'pairwise'", + "not": {} + } + } + }, + "examples": [ + { + "enabled": ["public", "pairwise"], + "pairwise": { + "salt": "some-random-salt" + } + } + ] + }, + "dynamic_client_registration": { + "type": "object", + "additionalProperties": false, + "description": "Configures OpenID Connect Dynamic Client Registration (exposed as admin endpoints /clients/...).", + "properties": { + "default_scope": { + "type": "array", + "description": "The OpenID Connect Dynamic Client Registration specification has no concept of whitelisting OAuth 2.0 Scope. If you want to expose Dynamic Client Registration, you should set the default scope enabled for newly registered clients. Keep in mind that users can overwrite this default by setting the \"scope\" key in the registration payload, effectively disabling the concept of whitelisted scopes.", + "items": { + "type": "string" + }, + "examples": [["openid", "offline", "offline_access"]] + } + } + } + } + }, + "urls": { + "type": "object", + "additionalProperties": false, + "properties": { + "self": { + "type": "object", + "additionalProperties": false, + "properties": { + "issuer": { + "type": "string", + "description": "This value will be used as the \"issuer\" in access and ID tokens. It must be specified and using HTTPS protocol, unless --dangerous-force-http is set. This should typically be equal to the public value.", + "format": "uri", + "examples": ["https://localhost:4444/"] + }, + "public": { + "type": "string", + "description": "This is the base location of the public endpoints of your ORY Hydra installation. This should typically be equal to the issuer value. If left unspecified, it falls back to the issuer value.", + "format": "uri", + "examples": ["https://localhost:4444/"] + } + } + }, + "login": { + "type": "string", + "description": "Sets the login endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-login.app/login"] + }, + "consent": { + "type": "string", + "description": "Sets the consent endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-consent.app/consent"] + }, + "logout": { + "type": "string", + "description": "Sets the logout endpoint. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-logout.app/logout"] + }, + "error": { + "type": "string", + "description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.", + "format": "uri", + "examples": ["https://my-error.app/error"] + }, + "post_logout_redirect": { + "type": "string", + "description": "When a user agent requests to logout, it will be redirected to this url afterwards per default.", + "format": "uri", + "examples": ["https://my-example.app/logout-successful"] + } + } + }, + "strategies": { + "type": "object", + "additionalProperties": false, + "properties": { + "scope": { + "type": "string", + "description": "Defines how scopes are matched. For more details have a look at https://github.com/ory/fosite#scopes", + "enum": [ + "exact", + "wildcard", + "DEPRECATED_HIERARCHICAL_SCOPE_STRATEGY" + ], + "default": "wildcard" + }, + "access_token": { + "type": "string", + "description": "Defines access token type. jwt is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens", + "enum": ["opaque", "jwt"] + } + } + }, + "ttl": { + "type": "object", + "additionalProperties": false, + "description": "Configures time to live.", + "properties": { + "login_consent_request": { + "description": "Configures how long a user login and consent flow may take.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "access_token": { + "description": "Configures how long access tokens are valid.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "refresh_token": { + "description": "Configures how long refresh tokens are valid. Set to -1 for refresh tokens to never expire.", + "default": "720h", + "oneOf": [ + { + "$ref": "#/definitions/duration" + }, + { + "enum": ["-1", -1] + } + ] + }, + "id_token": { + "description": "Configures how long id tokens are valid.", + "default": "1h", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + }, + "auth_code": { + "description": "Configures how long auth codes are valid.", + "default": "10m", + "allOf": [ + { + "$ref": "#/definitions/duration" + } + ] + } + } + }, + "oauth2": { + "type": "object", + "additionalProperties": false, + "properties": { + "expose_internal_errors": { + "type": "boolean", + "description": "Set this to true if you want to share error debugging information with your OAuth 2.0 clients. Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error codes and similar errors.", + "default": false, + "examples": [true] + }, + "session": { + "type": "object", + "properties": { + "encrypt_at_rest": { + "type": "boolean", + "default": true, + "title": "Encrypt OAuth2 Session", + "description": "If set to true (default) ORY Hydra encrypt OAuth2 and OpenID Connect session data using AES-GCM and the system secret before persisting it in the database." + } + } + }, + "include_legacy_error_fields": { + "type": "boolean", + "description": "Set this to true if you want to include the `error_hint` and `error_debug` legacy fields in error responses. We recommend to set this to `false` unless you have clients using these fields.", + "default": false, + "examples": [true] + }, + "hashers": { + "type": "object", + "additionalProperties": false, + "description": "Configures hashing algorithms. Supports only BCrypt at the moment.", + "properties": { + "bcrypt": { + "type": "object", + "additionalProperties": false, + "description": "Configures the BCrypt hashing algorithm used for hashing Client Secrets.", + "properties": { + "cost": { + "type": "integer", + "description": "Sets the BCrypt cost. The higher the value, the more CPU time is being used to generate hashes.", + "default": 10, + "minimum": 4, + "maximum": 31 + } + } + } + } + }, + "pkce": { + "type": "object", + "additionalProperties": false, + "properties": { + "enforced": { + "type": "boolean", + "description": "Sets whether PKCE should be enforced for all clients.", + "examples": [true] + }, + "enforced_for_public_clients": { + "type": "boolean", + "description": "Sets whether PKCE should be enforced for public clients.", + "examples": [true] + } + } + }, + "client_credentials": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_grant_allowed_scope": { + "type": "boolean", + "description": "Defines how scopes are added if the request doesn't contains any scope", + "examples": [false] + } + } + } + } + }, + "secrets": { + "type": "object", + "additionalProperties": false, + "description": "The secrets section configures secrets used for encryption and signing of several systems. All secrets can be rotated, for more information on this topic go to: https://www.ory.sh/docs/hydra/advanced#rotation-of-hmac-token-signing-and-database-and-cookie-encryption-keys", + "properties": { + "system": { + "description": "The system secret must be at least 16 characters long. If none is provided, one will be generated. They key is used to encrypt sensitive data using AES-GCM (256 bit) and validate HMAC signatures. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.", + "type": "array", + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "this-is-the-primary-secret", + "this-is-an-old-secret", + "this-is-another-old-secret" + ] + ] + }, + "cookie": { + "type": "array", + "description": "A secret that is used to encrypt cookie sessions. Defaults to secrets.system. It is recommended to use a separate secret in production. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.", + "items": { + "type": "string", + "minLength": 16 + }, + "examples": [ + [ + "this-is-the-primary-secret", + "this-is-an-old-secret", + "this-is-another-old-secret" + ] + ] + } + } + }, + "profiling": { + "type": "string", + "description": "Enables profiling if set. For more details on profiling, head over to: https://blog.golang.org/profiling-go-programs", + "enum": ["cpu", "mem"], + "examples": ["cpu"] + }, + "tracing": { + "$ref": "ory://tracing-config" + }, + "version": { + "type": "string", + "title": "The Hydra version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" + }, + "cgroups": { + "type": "object", + "additionalProperties": false, + "description": "ORY Hydra can respect Linux container CPU quota", + "properties": { + "v1": { + "type": "object", + "additionalProperties": false, + "description": "Configures parameters using cgroups v1 hierarchy", + "properties": { + "auto_max_procs_enabled": { + "type": "boolean", + "description": "Set GOMAXPROCS automatically according to cgroups limits", + "default": false, + "examples": [true] + } + } + } + } + } + }, + "required": ["dsn"] +} diff --git a/oryx/configx/stub/hydra/expected.json b/oryx/configx/stub/hydra/expected.json new file mode 100644 index 0000000..d35faf4 --- /dev/null +++ b/oryx/configx/stub/hydra/expected.json @@ -0,0 +1,122 @@ +{ + "cgroups": { + "v1": { + "auto_max_procs_enabled": false + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "log": { + "format": "text", + "leak_sensitive_values": false, + "level": "info" + }, + "oauth2": { + "expose_internal_errors": false, + "hashers": { + "bcrypt": { + "cost": 10 + } + }, + "include_legacy_error_fields": false, + "session": { + "encrypt_at_rest": true + } + }, + "oidc": { + "subject_identifiers": { + "enabled": ["pairwise", "public"], + "pairwise": { + "salt": "youReallyNeedToChangeThis" + } + } + }, + "secrets": { + "system": ["youReallyNeedToChangeThis"] + }, + "serve": { + "admin": { + "access_log": { + "disable_for_health": false + }, + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": false, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "", + "port": 4445, + "socket": { + "group": "", + "mode": 493, + "owner": "" + } + }, + "cookies": { + "same_site_legacy_workaround": false, + "same_site_mode": "Lax" + }, + "public": { + "access_log": { + "disable_for_health": false + }, + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": false, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "", + "port": 4444, + "socket": { + "group": "", + "mode": 493, + "owner": "" + } + } + }, + "strategies": { + "scope": "wildcard" + }, + "tracing": { + "provider": "jaeger", + "providers": { + "jaeger": { + "local_agent_address": "jaeger:6831", + "sampling": { + "server_url": "http://jaeger:5778/sampling" + } + } + } + }, + "ttl": { + "access_token": "1h", + "auth_code": "10m", + "id_token": "1h", + "login_consent_request": "1h", + "refresh_token": "720h" + }, + "urls": { + "consent": "http://127.0.0.1:3000/consent", + "login": "http://127.0.0.1:3000/login", + "logout": "http://127.0.0.1:3000/logout", + "self": { + "issuer": "http://127.0.0.1:4444" + } + }, + "webfinger": { + "jwks": { + "broadcast_keys": ["hydra.openid.id-token"] + } + } +} diff --git a/oryx/configx/stub/hydra/hydra.yaml b/oryx/configx/stub/hydra/hydra.yaml new file mode 100644 index 0000000..441dfc9 --- /dev/null +++ b/oryx/configx/stub/hydra/hydra.yaml @@ -0,0 +1,22 @@ +serve: + cookies: + same_site_mode: Lax + +urls: + self: + issuer: http://127.0.0.1:4444 + consent: http://127.0.0.1:3000/consent + login: http://127.0.0.1:3000/login + logout: http://127.0.0.1:3000/logout + +secrets: + system: + - youReallyNeedToChangeThis + +oidc: + subject_identifiers: + enabled: + - pairwise + - public + pairwise: + salt: youReallyNeedToChangeThis diff --git a/oryx/configx/stub/kratos/config.schema.json b/oryx/configx/stub/kratos/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/kratos/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/kratos/expected.json b/oryx/configx/stub/kratos/expected.json new file mode 100644 index 0000000..e9b5d45 --- /dev/null +++ b/oryx/configx/stub/kratos/expected.json @@ -0,0 +1,135 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": true, + "config": { + "providers": [ + { + "id": "google", + "provider": "google", + "mapper_url": "file:///etc/config/kratos/oidc.google.jsonnet", + "client_id": "client@example.com", + "client_secret": "secret" + } + ] + } + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/kratos/kratos.yaml b/oryx/configx/stub/kratos/kratos.yaml new file mode 100644 index 0000000..0d74f19 --- /dev/null +++ b/oryx/configx/stub/kratos/kratos.yaml @@ -0,0 +1,76 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + oidc: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/multi/a.yaml b/oryx/configx/stub/multi/a.yaml new file mode 100644 index 0000000..f3e1808 --- /dev/null +++ b/oryx/configx/stub/multi/a.yaml @@ -0,0 +1,27 @@ +version: v0.5.3-alpha.1 + +dsn: memory + +serve: + public: + base_url: http://127.0.0.1:4433/ + cors: + enabled: true + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://127.0.0.1:4455/ + whitelisted_return_urls: + - http://127.0.0.1:4455 + + methods: + password: + enabled: true + + flows: + error: + ui_url: http://127.0.0.1:4455/error + + settings: + ui_url: http://127.0.0.1:4455/settings diff --git a/oryx/configx/stub/multi/b.yaml b/oryx/configx/stub/multi/b.yaml new file mode 100644 index 0000000..1d48989 --- /dev/null +++ b/oryx/configx/stub/multi/b.yaml @@ -0,0 +1,54 @@ +selfservice: + flows: + settings: + privileged_session_max_age: 15m + + recovery: + enabled: true + ui_url: http://127.0.0.1:4455/recovery + + verification: + enabled: true + ui_url: http://127.0.0.1:4455/verify + after: + default_browser_return_url: http://127.0.0.1:4455/ + + logout: + after: + default_browser_return_url: http://127.0.0.1:4455/auth/login + + login: + ui_url: http://127.0.0.1:4455/auth/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://127.0.0.1:4455/auth/registration + after: + password: + hooks: + - hook: session + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + +hashers: + argon2: + parallelism: 1 + memory: 131072 + iterations: 2 + salt_length: 16 + key_length: 16 + +identity: + default_schema_url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true diff --git a/oryx/configx/stub/multi/config.schema.json b/oryx/configx/stub/multi/config.schema.json new file mode 100644 index 0000000..75847b2 --- /dev/null +++ b/oryx/configx/stub/multi/config.schema.json @@ -0,0 +1,1085 @@ +{ + "$id": "https://github.com/ory/kratos/.schema/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Kratos Configuration", + "type": "object", + "definitions": { + "defaultReturnTo": { + "title": "Redirect browsers to set URL per default", + "description": "ORY Kratos redirects to this URL per default on completion of self-service flows and other browser interaction. Read this [article for more information on browser redirects](https://www.ory.sh/kratos/docs/concepts/browser-redirect-flow-completion).", + "type": "string", + "format": "uri-reference", + "minLength": 1, + "examples": ["https://my-app.com/dashboard", "/dashboard"] + }, + "selfServiceSessionRevokerHook": { + "type": "object", + "properties": { + "hook": { + "const": "revoke_active_sessions" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceVerifyHook": { + "type": "object", + "properties": { + "hook": { + "const": "verify" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "selfServiceSessionIssuerHook": { + "type": "object", + "properties": { + "hook": { + "const": "session" + } + }, + "additionalProperties": false, + "required": ["hook"] + }, + "OIDCClaims": { + "title": "OpenID Connect claims", + "description": "The OpenID Connect claims and optionally their properties which should be included in the id_token or returned from the UserInfo Endpoint.", + "type": "object", + "examples": [ + { + "id_token": { + "email": null, + "email_verified": null + } + }, + { + "userinfo": { + "given_name": { + "essential": true + }, + "nickname": null, + "email": { + "essential": true + }, + "email_verified": { + "essential": true + }, + "picture": null, + "http://example.info/claims/groups": null + }, + "id_token": { + "auth_time": { + "essential": true + }, + "acr": { + "values": ["urn:mace:incommon:iap:silver"] + } + } + } + ], + "patternProperties": { + "^userinfo$|^id_token$": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".*": { + "oneOf": [ + { + "const": null, + "description": "Indicates that this Claim is being requested in the default manner." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "essential": { + "description": "Indicates whether the Claim being requested is an Essential Claim.", + "type": "boolean" + }, + "value": { + "description": "Requests that the Claim be returned with a particular value.", + "$comment": "There seem to be no constrains on value" + }, + "values": { + "description": "Requests that the Claim be returned with one of a set of values, with the values appearing in order of preference.", + "type": "array", + "items": { + "$comment": "There seem to be no constrains on individual items" + } + } + } + } + ] + } + } + } + } + }, + "selfServiceOIDCProvider": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + }, + "requested_claims": { + "$ref": "#/definitions/OIDCClaims" + } + }, + "additionalProperties": false, + "required": [ + "id", + "provider", + "client_id", + "client_secret", + "mapper_url" + ], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + }, + "selfServiceAfterSettingsMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceVerifyHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterLoginMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionRevokerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterRegistrationMethod": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "hooks": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/selfServiceSessionIssuerHook" + } + ] + }, + "uniqueItems": true, + "additionalItems": false + } + } + }, + "selfServiceAfterSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + }, + "profile": { + "$ref": "#/definitions/selfServiceAfterSettingsMethod" + } + } + }, + "selfServiceAfterLogin": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterLoginMethod" + } + } + }, + "selfServiceAfterRegistration": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "password": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + }, + "oidc": { + "$ref": "#/definitions/selfServiceAfterRegistrationMethod" + } + } + } + }, + "properties": { + "selfservice": { + "type": "object", + "additionalProperties": false, + "required": ["default_browser_return_url"], + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + }, + "whitelisted_return_urls": { + "title": "Whitelisted Return To URLs", + "description": "List of URLs that are allowed to be redirected to. A redirection request is made by appending `?return_to=...` to Login, Registration, and other self-service flows.", + "type": "array", + "items": { + "type": "string", + "format": "uri-reference" + }, + "examples": [ + [ + "https://app.my-app.com/dashboard", + "/dashboard", + "https://www.my-app.com/" + ] + ], + "uniqueItems": true + }, + "flows": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "URL of the Settings page.", + "description": "URL where the Settings UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/user/settings"], + "default": "https://www.ory.sh/kratos/docs/fallback/settings" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "privileged_session_max_age": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterSettings" + } + } + }, + "logout": { + "type": "object", + "additionalProperties": false, + "properties": { + "after": { + "type": "object", + "additionalProperties": false, + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + } + } + } + }, + "registration": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Registration UI URL", + "description": "URL where the Registration UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/signup"], + "default": "https://www.ory.sh/kratos/docs/fallback/registration" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterRegistration" + } + } + }, + "login": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "Login UI URL", + "description": "URL where the Login UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/login"], + "default": "https://www.ory.sh/kratos/docs/fallback/login" + }, + "lifespan": { + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + }, + "after": { + "$ref": "#/definitions/selfServiceAfterLogin" + } + } + }, + "verification": { + "title": "Email and Phone Verification and Account Activation Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Email/Phone Verification", + "description": "If set to true will enable [Email and Phone Verification and Account Activation](https://www.ory.sh/kratos/docs/self-service/flows/verify-email-account-activation/).", + "default": false + }, + "ui_url": { + "title": "Verify UI URL", + "description": "URL where the ORY Verify UI is hosted. This is the page where users activate and / or verify their email or telephone number. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/verification" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Verification Request Lifespan", + "description": "Sets how long the verification request (for the UI interaction) is valid.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "recovery": { + "title": "Account Recovery Configuration", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enable Account Recovery", + "description": "If set to true will enable [Account Recovery](https://www.ory.sh/kratos/docs/self-service/flows/password-reset-account-recovery/).", + "default": false + }, + "ui_url": { + "title": "Recovery UI URL", + "description": "URL where the ORY Recovery UI is hosted. This is the page where users request and complete account recovery. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/verify"], + "default": "https://www.ory.sh/kratos/docs/fallback/recovery" + }, + "after": { + "type": "object", + "properties": { + "default_browser_return_url": { + "$ref": "#/definitions/defaultReturnTo" + } + }, + "additionalProperties": false + }, + "lifespan": { + "title": "Self-Service Recovery Request Lifespan", + "description": "Sets how long the recovery request is valid. If expired, the user has to redo the flow.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1h", + "examples": ["1h", "1m", "1s"] + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "properties": { + "ui_url": { + "title": "ORY Kratos Error UI URL", + "description": "URL where the ORY Kratos Error UI is hosted. Check the [reference implementation](https://github.com/ory/kratos-selfservice-ui-node).", + "type": "string", + "format": "uri-reference", + "examples": ["https://my-app.com/kratos-error"], + "default": "https://www.ory.sh/kratos/docs/fallback/error" + } + } + } + } + }, + "methods": { + "type": "object", + "additionalProperties": false, + "properties": { + "profile": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Profile Management Method", + "default": true + } + } + }, + "link": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Link Method", + "default": true + } + } + }, + "password": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables Username/Email and Password Method", + "default": true + } + } + }, + "oidc": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "title": "Enables OpenID Connect Method", + "default": false + }, + "config": { + "type": "object", + "additionalProperties": false, + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "$ref": "#/definitions/selfServiceOIDCProvider" + } + } + } + } + } + } + } + } + } + }, + "dsn": { + "type": "string", + "title": "Data Source Name", + "description": "DSN is used to specify the database credentials as a connection URI.", + "examples": [ + "postgres://user: password@postgresd:5432/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "mysql://user:secret@tcp(mysqld:3306)/database?max_conns=20&max_idle_conns=4", + "cockroach://user@cockroachdb:26257/database?sslmode=disable&max_conns=20&max_idle_conns=4", + "sqlite:///var/lib/sqlite/db.sqlite?_fk=true&mode=rwc" + ] + }, + "courier": { + "type": "object", + "title": "Courier configuration", + "description": "The courier is responsible for sending and delivering messages over email, sms, and other means.", + "properties": { + "template_override_path": { + "type": "string", + "title": "Override message templates", + "description": "You can override certain or all message templates by pointing this key to the path where the templates are located.", + "examples": ["/conf/courier-templates"] + }, + "smtp": { + "title": "SMTP Configuration", + "description": "Configures outgoing emails using the SMTP protocol.", + "type": "object", + "properties": { + "connection_uri": { + "title": "SMTP connection string", + "description": "This URI will be used to connect to the SMTP server. Use the query parameter to allow (`?skip_ssl_verify=true`) or disallow (`?skip_ssl_verify=false`) self-signed TLS certificates. Please keep in mind that any host other than localhost / 127.0.0.1 must use smtp over TLS (smtps) or the connection will not be possible.", + "examples": [ + "smtps://foo:bar@my-mailserver:1234/?skip_ssl_verify=false" + ], + "type": "string", + "format": "uri" + }, + "from_address": { + "title": "SMTP Sender Address", + "description": "The recipient of an email will see this as the sender address.", + "type": "string", + "format": "email", + "default": "no-reply@ory.kratos.sh" + } + }, + "required": ["connection_uri"], + "additionalProperties": false + } + }, + "required": ["smtp"], + "additionalProperties": false + }, + "serve": { + "type": "object", + "properties": { + "admin": { + "type": "object", + "properties": { + "base_url": { + "title": "Admin Base URL", + "description": "The URL where the admin endpoint is exposed at.", + "type": "string", + "format": "uri", + "examples": ["https://kratos.private-network:4434/"] + }, + "host": { + "title": "Admin Host", + "description": "The host (interface) kratos' admin endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Admin Port", + "description": "The port kratos' admin endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4434], + "default": 4434 + } + }, + "additionalProperties": false + }, + "public": { + "type": "object", + "properties": { + "cors": { + "type": "object", + "additionalProperties": false, + "description": "Configures Cross Origin Resource Sharing for public endpoints.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Sets whether CORS is enabled.", + "default": false + }, + "allowed_origins": { + "type": "array", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.", + "items": { + "type": "string", + "minLength": 1, + "not": { + "type": "string", + "description": "does match all strings that contain two or more (*)", + "pattern": ".*\\*.*\\*.*" + }, + "anyOf": [ + { + "format": "uri" + }, + { + "const": "*" + } + ] + }, + "uniqueItems": true, + "default": ["*"], + "examples": [ + [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + ] + }, + "allowed_methods": { + "type": "array", + "description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.", + "default": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "items": { + "type": "string", + "enum": [ + "POST", + "GET", + "PUT", + "PATCH", + "DELETE", + "CONNECT", + "HEAD", + "OPTIONS", + "TRACE" + ] + } + }, + "allowed_headers": { + "type": "array", + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "default": [ + "Authorization", + "Content-Type", + "X-Session-Token" + ], + "items": { + "type": "string" + } + }, + "exposed_headers": { + "type": "array", + "description": "Sets which headers are safe to expose to the API of a CORS API specification.", + "default": ["Content-Type"], + "items": { + "type": "string" + } + }, + "allow_credentials": { + "type": "boolean", + "description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "default": true + }, + "options_passthrough": { + "type": "boolean", + "description": "TODO", + "default": false + }, + "max_age": { + "type": "integer", + "description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.", + "default": 0, + "minimum": 0 + }, + "debug": { + "type": "boolean", + "description": "Adds additional log output to debug server side CORS issues.", + "default": false + } + } + }, + "base_url": { + "title": "Public Base URL", + "description": "The URL where the public endpoint is exposed at.", + "type": "string", + "format": "uri-reference", + "examples": [ + "https://my-app.com/.ory/kratos/public", + "/.ory/kratos/public/" + ] + }, + "host": { + "title": "Public Host", + "description": "The host (interface) kratos' public endpoint listens on.", + "type": "string", + "default": "0.0.0.0" + }, + "port": { + "title": "Public Port", + "description": "The port kratos' public endpoint listens on.", + "type": "integer", + "minimum": 1, + "maximum": 65535, + "examples": [4433], + "default": 4433 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "log": { + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": [ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + "panic" + ] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs." + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "format": { + "type": "string", + "enum": ["json", "text"] + } + }, + "additionalProperties": false + }, + "identity": { + "type": "object", + "properties": { + "default_schema_url": { + "title": "JSON Schema URL for default identity traits", + "description": "Path to the JSON Schema which describes a default identity's traits.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + }, + "schemas": { + "type": "array", + "title": "Additional JSON Schemas for Identity Traits", + "examples": [ + [ + { + "id": "customer", + "url": "https://foo.bar.com/path/to/customer.traits.schema.json" + }, + { + "id": "employee", + "url": "https://foo.bar.com/path/to/employee.traits.schema.json" + }, + { + "id": "employee-v2", + "url": "https://foo.bar.com/path/to/employee.v2.traits.schema.json" + } + ] + ], + "items": { + "type": "object", + "properties": { + "id": { + "title": "The schema's ID.", + "type": "string", + "examples": ["employee"] + }, + "url": { + "type": "string", + "title": "Path to the JSON Schema", + "format": "uri", + "examples": [ + "file://path/to/identity.traits.schema.json", + "https://foo.bar.com/path/to/identity.traits.schema.json" + ] + } + }, + "required": ["id", "url"], + "not": { + "type": "object", + "properties": { + "id": { + "const": "default" + } + }, + "additionalProperties": true + } + } + } + }, + "required": ["default_schema_url"], + "additionalProperties": false + }, + "secrets": { + "type": "object", + "properties": { + "default": { + "type": "array", + "title": "Default Encryption Signing Secrets", + "description": "The first secret in the array is used for singing and encrypting things while all other keys are used to verify and decrypt older things that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + }, + "cookie": { + "type": "array", + "title": "Singing Keys for Cookies", + "description": "The first secret in the array is used for encrypting cookies while all other keys are used to decrypt older cookies that were signed with that old secret.", + "items": { + "type": "string", + "minLength": 16 + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "hashers": { + "title": "Hashing Algorithm Configuration", + "type": "object", + "properties": { + "argon2": { + "title": "Configuration for the Argon2id hasher.", + "type": "object", + "properties": { + "memory": { + "type": "integer", + "minimum": 16384 + }, + "iterations": { + "type": "integer", + "minimum": 1 + }, + "parallelism": { + "type": "integer", + "minimum": 1 + }, + "salt_length": { + "type": "integer", + "minimum": 16 + }, + "key_length": { + "type": "integer", + "minimum": 16 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "session": { + "type": "object", + "additionalProperties": false, + "properties": { + "lifespan": { + "title": "Session Lifespan", + "description": "Defines how long a session is active. Once that lifespan has been reached, the user needs to sign in again.", + "type": "string", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "24h", + "examples": ["1h", "1m", "1s"] + }, + "cookie": { + "type": "object", + "properties": { + "domain": { + "title": "Session Cookie Domain", + "description": "Sets the session cookie domain. Useful when dealing with subdomains. Use with care!", + "type": "string" + }, + "persistent": { + "title": "Make Session Cookie Persistent", + "description": "If set to true will persist the cookie in the end-user's browser using the `max-age` parameter which is set to the `session.lifespan` value. Persistent cookies are not deleted when the browser is closed (e.g. on reboot or alt+f4).", + "type": "boolean", + "default": true + }, + "path": { + "title": "Session Cookie Path", + "description": "Sets the session cookie path. Use with care!", + "type": "string", + "default": "/" + }, + "same_site": { + "title": "Cookie Same Site Configuration", + "type": "string", + "enum": ["Strict", "Lax", "None"], + "default": "Lax" + } + }, + "additionalProperties": false + } + } + }, + "version": { + "title": "The kratos version this config is written for.", + "description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.", + "type": "string", + "pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "examples": ["v0.5.0-alpha.1"] + } + }, + "allOf": [ + { + "if": { + "properties": { + "selfservice": { + "properties": { + "flows": { + "oneOf": [ + { + "properties": { + "verification": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["verification"] + }, + { + "properties": { + "recovery": { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["enabled"] + } + }, + "required": ["recovery"] + } + ] + } + }, + "required": ["flows"] + } + }, + "required": ["selfservice"] + }, + "then": { + "required": ["courier"] + } + } + ], + "required": ["identity", "dsn", "selfservice"] +} diff --git a/oryx/configx/stub/multi/expected.json b/oryx/configx/stub/multi/expected.json new file mode 100644 index 0000000..2fe5dd1 --- /dev/null +++ b/oryx/configx/stub/multi/expected.json @@ -0,0 +1,124 @@ +{ + "courier": { + "smtp": { + "connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true", + "from_address": "no-reply@ory.kratos.sh" + } + }, + "dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true", + "hashers": { + "argon2": { + "iterations": 2, + "key_length": 16, + "memory": 131072, + "parallelism": 1, + "salt_length": 16 + } + }, + "identity": { + "default_schema_url": "file:///etc/config/kratos/identity.schema.json" + }, + "log": { + "format": "text", + "leak_sensitive_values": true, + "level": "debug" + }, + "secrets": { + "cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"] + }, + "selfservice": { + "default_browser_return_url": "http://127.0.0.1:4455/", + "flows": { + "error": { + "ui_url": "http://127.0.0.1:4455/error" + }, + "login": { + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/login" + }, + "logout": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/auth/login" + } + }, + "recovery": { + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/recovery" + }, + "registration": { + "after": { + "password": { + "hooks": [ + { + "hook": "session" + } + ] + } + }, + "lifespan": "10m", + "ui_url": "http://127.0.0.1:4455/auth/registration" + }, + "settings": { + "lifespan": "1h", + "privileged_session_max_age": "15m", + "ui_url": "http://127.0.0.1:4455/settings" + }, + "verification": { + "after": { + "default_browser_return_url": "http://127.0.0.1:4455/" + }, + "enabled": true, + "lifespan": "1h", + "ui_url": "http://127.0.0.1:4455/verify" + } + }, + "methods": { + "link": { + "enabled": true + }, + "oidc": { + "enabled": false + }, + "password": { + "enabled": true + }, + "profile": { + "enabled": true + } + }, + "whitelisted_return_urls": ["http://127.0.0.1:4455"] + }, + "serve": { + "admin": { + "base_url": "http://kratos:4434/", + "host": "0.0.0.0", + "port": 4434 + }, + "public": { + "base_url": "http://127.0.0.1:4433/", + "cors": { + "allow_credentials": true, + "allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"], + "allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"], + "allowed_origins": ["*"], + "debug": false, + "enabled": true, + "exposed_headers": ["Content-Type"], + "max_age": 0, + "options_passthrough": false + }, + "host": "0.0.0.0", + "port": 4433 + } + }, + "session": { + "cookie": { + "path": "/", + "persistent": true, + "same_site": "Lax" + }, + "lifespan": "24h" + }, + "version": "v0.5.3-alpha.1" +} diff --git a/oryx/configx/stub/nested-array/config.schema.json b/oryx/configx/stub/nested-array/config.schema.json new file mode 100644 index 0000000..b70c935 --- /dev/null +++ b/oryx/configx/stub/nested-array/config.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + } + }, + "additionalProperties": false, + "required": [], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + } + } + } +} diff --git a/oryx/configx/stub/nested-array/expected.json b/oryx/configx/stub/nested-array/expected.json new file mode 100644 index 0000000..e8609d2 --- /dev/null +++ b/oryx/configx/stub/nested-array/expected.json @@ -0,0 +1,11 @@ +{ + "providers": [ + { + "id": "google", + "client_id": "client@example.com" + }, + { + "client_id": "some@example.com" + } + ] +} diff --git a/oryx/configx/stub/nested-array/kratos.yaml b/oryx/configx/stub/nested-array/kratos.yaml new file mode 100644 index 0000000..ac667ff --- /dev/null +++ b/oryx/configx/stub/nested-array/kratos.yaml @@ -0,0 +1,2 @@ +providers: + - id: google diff --git a/oryx/configx/stub/watch/config.schema.json b/oryx/configx/stub/watch/config.schema.json new file mode 100644 index 0000000..80382f4 --- /dev/null +++ b/oryx/configx/stub/watch/config.schema.json @@ -0,0 +1,19 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "config", + "type": "object", + "properties": { + "dsn": { + "type": "string" + }, + "foo": { + "const": "bar" + }, + "bar": { + "type": "string", + "enum": ["foo", "bar", "baz"] + } + }, + "required": ["dsn"] +} diff --git a/oryx/configx/tls.schema.json b/oryx/configx/tls.schema.json new file mode 100644 index 0000000..832f679 --- /dev/null +++ b/oryx/configx/tls.schema.json @@ -0,0 +1,68 @@ +{ + "$id": "https://github.com/ory/x/tlsx/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + }, + "key": { + "title": "Private Key (PEM)", + "$ref": "#/definitions/source" + }, + "cert": { + "title": "TLS Certificate (PEM)", + "$ref": "#/definitions/source" + }, + "allow_termination_from": { + "type": "array", + "description": "Allow-list one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. The service serves http instead of https when this option is set.", + "items": { + "description": "CIDR address range.", + "type": "string", + "oneOf": [ + { + "pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + }, + { + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$" + } + ], + "examples": ["127.0.0.1/32"] + } + } + }, + "definitions": { + "source": { + "type": "object", + "oneOf": [ + { + "properties": { + "path": { + "title": "Path to PEM-encoded File", + "type": "string", + "examples": ["path/to/file.pem"] + } + }, + "additionalProperties": false + }, + { + "properties": { + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/oryx/contextx/contextual.go b/oryx/contextx/contextual.go new file mode 100644 index 0000000..e7d7446 --- /dev/null +++ b/oryx/contextx/contextual.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/ory/x/configx" + + "github.com/gofrs/uuid" +) + +type ( + Contextualizer interface { + // Network returns the network id for the given context. + Network(ctx context.Context, network uuid.UUID) uuid.UUID + + // Config returns the config for the given context. + Config(ctx context.Context, config *configx.Provider) *configx.Provider + } + Provider interface { + Contextualizer() Contextualizer + } + Static struct { + NID uuid.UUID + C *configx.Provider + } +) + +func (d *Static) Network(_ context.Context, nid uuid.UUID) uuid.UUID { + if d.NID == uuid.Nil { + return nid + } + return d.NID +} + +func (d *Static) Config(_ context.Context, c *configx.Provider) *configx.Provider { + if d.C == nil { + return c + } + return d.C +} diff --git a/oryx/contextx/contextual_mock.go b/oryx/contextx/contextual_mock.go new file mode 100644 index 0000000..ef75861 --- /dev/null +++ b/oryx/contextx/contextual_mock.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/ory/x/configx" + + "github.com/gofrs/uuid" +) + +// TestContextualizer is a mock implementation of the Contextualizer interface. +type TestContextualizer struct{} + +type contextKeyFake int + +// fakeNIDContext is a test key for NID. +const fakeNIDContext contextKeyFake = 1 + +// SetNIDContext sets the nid for the given context. +func SetNIDContext(ctx context.Context, nid uuid.UUID) context.Context { + return context.WithValue(ctx, fakeNIDContext, nid) //nolint:staticcheck +} + +// Network returns the network id for the given context. +func (d *TestContextualizer) Network(ctx context.Context, network uuid.UUID) uuid.UUID { + nid, ok := ctx.Value(fakeNIDContext).(uuid.UUID) + if !ok { + return network + } + return nid +} + +// Config returns the config for the given context. +func (d *TestContextualizer) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + return config +} diff --git a/oryx/contextx/default.go b/oryx/contextx/default.go new file mode 100644 index 0000000..5733807 --- /dev/null +++ b/oryx/contextx/default.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/configx" +) + +type Default struct{} + +var _ Contextualizer = (*Default)(nil) + +func (d *Default) Network(_ context.Context, network uuid.UUID) uuid.UUID { + if network == uuid.Nil { + panic("nid must be not nil") + } + return network +} + +func (d *Default) Config(_ context.Context, config *configx.Provider) *configx.Provider { + return config +} diff --git a/oryx/contextx/testhelpers.go b/oryx/contextx/testhelpers.go new file mode 100644 index 0000000..98e835b --- /dev/null +++ b/oryx/contextx/testhelpers.go @@ -0,0 +1,158 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + "net/http" + "net/http/httptest" + + "github.com/gofrs/uuid" + + "github.com/ory/x/configx" +) + +type ( + TestConfigProvider struct { + ConfigSchema []byte + Options []configx.OptionModifier + } + contextKey int +) + +func NewTestConfigProvider(schema []byte, opts ...configx.OptionModifier) *TestConfigProvider { + return &TestConfigProvider{ + ConfigSchema: schema, + Options: opts, + } +} + +func (t *TestConfigProvider) Network(ctx context.Context, network uuid.UUID) uuid.UUID { + return (&Default{}).Network(ctx, network) +} + +func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + return config + } + + opts := make([]configx.OptionModifier, 1, 1+len(values)) + opts[0] = configx.WithValues(config.All()) + for _, v := range values { + opts = append(opts, configx.WithValues(v)) + } + config, err := configx.New(ctx, t.ConfigSchema, append(t.Options, opts...)...) + if err != nil { + // This is not production code. The provider is only used in tests. + panic(err) + } + return config +} + +const contextConfigKey contextKey = 1 + +var ( + _ Contextualizer = (*TestConfigProvider)(nil) +) + +func WithConfigValue(ctx context.Context, key string, value any) context.Context { + return WithConfigValues(ctx, map[string]any{key: value}) +} + +func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context { + values, ok := ctx.Value(contextConfigKey).([]map[string]any) + if !ok { + values = make([]map[string]any, 0) + } + newValues := make([]map[string]any, len(values), len(values)+len(setValues)) + copy(newValues, values) + newValues = append(newValues, setValues...) + + return context.WithValue(ctx, contextConfigKey, newValues) +} + +type ConfigurableTestHandler struct { + configs map[uuid.UUID][]map[string]any + handler http.Handler +} + +func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler { + return &ConfigurableTestHandler{ + configs: make(map[uuid.UUID][]map[string]any), + handler: h, + } +} + +func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cID := r.Header.Get("Test-Config-Id") + if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok { + r = r.WithContext(WithConfigValues(r.Context(), config...)) + } + t.handler.ServeHTTP(w, r) +} + +func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID { + id := uuid.Must(uuid.NewV4()) + t.configs[id] = config + return id +} + +func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request { + r.Header.Set("Test-Config-Id", id.String()) + return r +} + +func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request { + return t.UseConfig(r, t.RegisterConfig(values...)) +} + +type ConfigurableTestServer struct { + *httptest.Server + handler *ConfigurableTestHandler + transport http.RoundTripper +} + +func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer { + handler := NewConfigurableTestHandler(h) + server := httptest.NewServer(handler) + + t := server.Client().Transport + cts := &ConfigurableTestServer{ + handler: handler, + Server: server, + transport: t, + } + server.Client().Transport = cts + return cts +} + +func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) { + config, ok := r.Context().Value(contextConfigKey).([]map[string]any) + if ok && config != nil { + r = t.handler.UseConfigValues(r, config...) + } + return t.transport.RoundTrip(r) +} + +type AutoContextClient struct { + *http.Client + transport http.RoundTripper + ctx context.Context +} + +func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient { + baseClient := *t.Server.Client() + autoClient := &AutoContextClient{ + Client: &baseClient, + transport: t, + ctx: ctx, + } + baseClient.Transport = autoClient + return autoClient +} + +func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) { + return c.transport.RoundTrip(r.WithContext(c.ctx)) +} diff --git a/oryx/contextx/tree.go b/oryx/contextx/tree.go new file mode 100644 index 0000000..125e8a3 --- /dev/null +++ b/oryx/contextx/tree.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package contextx + +import ( + "context" + "testing" +) + +type ContextKey int + +const ( + ValidContextKey ContextKey = iota + 1 +) + +var RootContext = context.WithValue(context.Background(), ValidContextKey, true) + +func TestRootContext(t testing.TB) context.Context { + return context.WithValue(t.Context(), ValidContextKey, true) +} + +func IsRootContext(ctx context.Context) bool { + is, ok := ctx.Value(ValidContextKey).(bool) + return is && ok +} diff --git a/oryx/cookiex/cookiex.go b/oryx/cookiex/cookiex.go new file mode 100644 index 0000000..3ebcf07 --- /dev/null +++ b/oryx/cookiex/cookiex.go @@ -0,0 +1,266 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package cookiex encodes authenticated, encrypted HTTP cookies. Values are +// sealed with the Ory-wide AEAD (see ory/x/aead) under keys derived from the +// configured secrets with HKDF-SHA256, and bound to their purpose and cookie +// name. The package replaces the gorilla/securecookie-based cookie stores; a +// decode-only fallback reads cookies minted by those stores until they have +// expired. +package cookiex + +import ( + "crypto/hkdf" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/json" + "net/http" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/aead" +) + +const ( + // formatPrefix marks the versioned wire format. A dot is not part of the + // base64url alphabet, so legacy securecookie values can never collide + // with the prefix. + formatPrefix = "v1." + // kdfInfo domain-separates the HKDF key derivation. + kdfInfo = "ory/x/cookiex/v1" + // aadPrefix domain-separates the additional authenticated data. + aadPrefix = "ory/x/cookiex/v1" + // defaultMaxAge matches the gorilla/securecookie default that the + // previous cookie stores relied on. + defaultMaxAge = 30 * 24 * time.Hour + // maxCookieValueLength is the browser limit that securecookie also + // enforced. + maxCookieValueLength = 4096 +) + +// ErrInvalidCookie is returned when a cookie with the requested name is +// present but none of its values could be decoded (or none matched). +var ErrInvalidCookie = errors.New("cookiex: cookie could not be decoded") + +type ( + // Option configures a Codec. + Option func(*config) + + config struct { + maxAge time.Duration + legacyKeyPairs [][]byte + legacyEncode bool + } +) + +// WithMaxAge overrides how old a cookie may be before decoding rejects it. +// The default is 30 days; zero disables the check. The value must not be +// negative. +func WithMaxAge(d time.Duration) Option { + return func(c *config) { c.maxAge = d } +} + +// Codec seals and opens cookie values of type T. T is serialized as JSON. +// A Codec is safe for concurrent use. +type Codec[T any] struct { + purpose string + keys [][32]byte + maxAge time.Duration + legacy legacyState + now func() time.Time +} + +// New returns a codec for the given purpose. The purpose is bound into the +// ciphertext and used as the metric label; it must be a short constant like +// "kratos/session". Because the purpose is embedded in the additional +// authenticated data, it must be non-empty and must not contain a pipe +// character. The codec seals with a key derived from the first secret +// and opens with keys derived from any of them, so secrets rotate by +// prepending a new one. +func New[T any](purpose string, secrets [][]byte, opts ...Option) (*Codec[T], error) { + if purpose == "" || strings.Contains(purpose, "|") { + return nil, errors.New("cookiex: purpose must be non-empty and must not contain a pipe character") + } + if len(secrets) == 0 { + return nil, errors.New("cookiex: at least one secret is required") + } + cfg := config{maxAge: defaultMaxAge} + for _, opt := range opts { + opt(&cfg) + } + if cfg.maxAge < 0 { + return nil, errors.New("cookiex: max age must not be negative") + } + if cfg.legacyEncode && len(cfg.legacyKeyPairs) == 0 { + return nil, errors.New("cookiex: legacy encode requires legacy key pairs") + } + keys := make([][32]byte, len(secrets)) + for i, secret := range secrets { + key, err := hkdf.Key(sha256.New, secret, nil, kdfInfo, 32) + if err != nil { + return nil, errors.Wrap(err, "cookiex: cannot derive key") + } + keys[i] = [32]byte(key) + } + return &Codec[T]{ + purpose: purpose, + keys: keys, + maxAge: cfg.maxAge, + legacy: newLegacyState(cfg, cfg.maxAge), + now: time.Now, + }, nil +} + +// envelope wraps the JSON payload with the seal time so decoding can enforce +// the max age. +type envelope struct { + IssuedAt int64 `json:"iat"` + Values json.RawMessage `json:"v"` +} + +// aad binds a ciphertext to this codec's purpose and the cookie name, so a +// sealed value cannot be replayed as a different cookie or in a different +// context, even under the same key. +func (c *Codec[T]) aad(name string) []byte { + return []byte(aadPrefix + "|" + c.purpose + "|" + name) +} + +func (c *Codec[T]) seal(name string, value T) (string, error) { + payload, err := json.Marshal(value) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal cookie value") + } + plaintext, err := json.Marshal(envelope{IssuedAt: c.now().Unix(), Values: payload}) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal envelope") + } + a, err := aead.New(c.keys[0]) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot create AEAD") + } + // The nonce is prepended to the ciphertext. AEADs that manage the nonce + // internally report a nonce size of zero, so this also covers them. + nonce := make([]byte, a.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", errors.Wrap(err, "cookiex: cannot generate nonce") + } + sealed := a.Seal(nonce, nonce, plaintext, c.aad(name)) + return formatPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil +} + +func (c *Codec[T]) open(name, value string) (T, error) { + var zero T + raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(value, formatPrefix)) + if err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + for _, key := range c.keys { + a, err := aead.New(key) + if err != nil { + return zero, errors.Wrap(err, "cookiex: cannot create AEAD") + } + if len(raw) < a.NonceSize() { + continue + } + plaintext, err := a.Open(nil, raw[:a.NonceSize()], raw[a.NonceSize():], c.aad(name)) + if err != nil { + continue + } + var env envelope + if err := json.Unmarshal(plaintext, &env); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + if c.maxAge > 0 && c.now().Sub(time.Unix(env.IssuedAt, 0)) > c.maxAge { + return zero, errors.WithStack(ErrInvalidCookie) + } + var out T + if err := json.Unmarshal(env.Values, &out); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + return out, nil + } + return zero, errors.WithStack(ErrInvalidCookie) +} + +// Set seals value into cookie.Value and writes the cookie to w. All other +// attributes (name, path, domain, max-age, secure, http-only, same-site) must +// be set by the caller on the cookie. +func (c *Codec[T]) Set(w http.ResponseWriter, cookie *http.Cookie, value T) error { + var encoded string + var err error + if c.legacy.encode { + encoded, err = c.sealLegacy(cookie.Name, value) + } else { + encoded, err = c.seal(cookie.Name, value) + } + if err != nil { + return err + } + if len(encoded) > maxCookieValueLength { + return errors.Errorf("cookiex: encoded cookie %q exceeds %d bytes", cookie.Name, maxCookieValueLength) + } + cookie.Value = encoded + http.SetCookie(w, cookie) + return nil +} + +// Get returns the value of the first cookie named name that decodes. It +// returns http.ErrNoCookie when no cookie with that name is present, and +// ErrInvalidCookie when none of the present values decode. +func (c *Codec[T]) Get(r *http.Request, name string) (T, error) { + return c.GetMatching(r, name, nil) +} + +// GetMatching returns the value of the first cookie named name that decodes +// and matches. Requests can carry multiple cookies with the same name, for +// example one from a parent and one from a sub domain; match selects among +// them. A nil match matches any value. It returns http.ErrNoCookie when no +// cookie with that name is present, and ErrInvalidCookie when none of the +// present values decode and match. +func (c *Codec[T]) GetMatching(r *http.Request, name string, match func(T) bool) (T, error) { + if match == nil { + match = func(T) bool { return true } + } + var zero T + found := false + for _, ck := range r.Cookies() { + if ck.Name != name { + continue + } + found = true + // Refuse oversized values before doing any decode work, matching the + // limit Set enforces and securecookie's own decode-side check. + if len(ck.Value) > maxCookieValueLength { + continue + } + var value T + var err error + if strings.HasPrefix(ck.Value, formatPrefix) { + value, err = c.open(name, ck.Value) + } else { + value, err = c.openLegacy(name, ck.Value) + } + if err != nil { + continue + } + if match(value) { + return value, nil + } + } + if !found { + return zero, errors.WithStack(http.ErrNoCookie) + } + return zero, errors.WithStack(ErrInvalidCookie) +} + +// Expire overwrites the cookie with an expired empty one, deleting it from +// the client. +func (c *Codec[T]) Expire(w http.ResponseWriter, cookie *http.Cookie) { + cookie.Value = "" + cookie.MaxAge = -1 + cookie.Expires = time.Unix(1, 0) + http.SetCookie(w, cookie) +} diff --git a/oryx/cookiex/legacy_securecookie.go b/oryx/cookiex/legacy_securecookie.go new file mode 100644 index 0000000..b4549e4 --- /dev/null +++ b/oryx/cookiex/legacy_securecookie.go @@ -0,0 +1,136 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package cookiex + +// This file isolates the gorilla/securecookie dependency. It exists so that +// cookies sealed by the previous securecookie-based stores keep decoding +// during the migration, and (behind WithLegacyEncode) so that freshly minted +// cookies stay readable by old pods during the stage-1 rollout. Delete it, +// and the securecookie dependency, once ory_legacy_cookie_decodes_total has +// flatlined fleet-wide for a full max-cookie-lifespan window. + +import ( + "crypto/sha256" + "encoding/json" + "time" + + "github.com/gorilla/securecookie" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// legacyDecodes counts cookies that were successfully decoded via the legacy +// securecookie fallback. It is registered on the Prometheus default +// registerer, which Kratos and Hydra expose on /metrics/prometheus. +var legacyDecodes = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "ory_legacy_cookie_decodes_total", + Help: "Number of cookies successfully decoded via the legacy securecookie fallback, by codec purpose.", +}, []string{"purpose"}) + +// LegacySecureCookieKeyPairs derives the (hashKey, blockKey) pairs the +// securecookie-based cookie stores used: each secret signs, and its SHA-256 +// digest encrypts. +func LegacySecureCookieKeyPairs(secrets [][]byte) [][]byte { + pairs := make([][]byte, 0, 2*len(secrets)) + for _, secret := range secrets { + block := sha256.Sum256(secret) + pairs = append(pairs, secret, block[:]) + } + return pairs +} + +// WithLegacyKeyPairs enables decoding cookies in the legacy securecookie +// format, sealed under the given (hashKey, blockKey) pairs. While this option +// is in use, T must be unmarshalable from a flat JSON object with string +// values; WithLegacyEncode additionally requires T to marshal to one. Remove +// once all cookies sealed in the legacy format have expired. +func WithLegacyKeyPairs(pairs ...[]byte) Option { + return func(c *config) { c.legacyKeyPairs = pairs } +} + +type legacyState struct { + codecs []securecookie.Codec + encode bool +} + +func newLegacyState(cfg config, maxAge time.Duration) legacyState { + codecs := securecookie.CodecsFromPairs(cfg.legacyKeyPairs...) + for _, codec := range codecs { + if sc, ok := codec.(*securecookie.SecureCookie); ok { + sc.MaxAge(int(maxAge / time.Second)) + } + } + return legacyState{codecs: codecs, encode: cfg.legacyEncode} +} + +// openLegacy decodes a securecookie-sealed value and bridges it into T via +// its JSON representation: legacy payloads are flat string-to-string maps, so +// string, time.Time, and UUID fields all round-trip through their JSON string +// forms. +func (c *Codec[T]) openLegacy(name, value string) (T, error) { + var zero T + if len(c.legacy.codecs) == 0 { + return zero, errors.WithStack(ErrInvalidCookie) + } + values := make(map[any]any) + if err := securecookie.DecodeMulti(name, value, &values, c.legacy.codecs...); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + flat := make(map[string]string, len(values)) + for k, v := range values { + key, keyOK := k.(string) + val, valOK := v.(string) + if !keyOK || !valOK { + return zero, errors.WithStack(ErrInvalidCookie) + } + flat[key] = val + } + bridge, err := json.Marshal(flat) + if err != nil { + return zero, errors.Wrap(err, "cookiex: cannot bridge legacy cookie") + } + var out T + if err := json.Unmarshal(bridge, &out); err != nil { + return zero, errors.WithStack(ErrInvalidCookie) + } + legacyDecodes.WithLabelValues(c.purpose).Inc() + return out, nil +} + +// WithLegacyEncode makes Set seal in the legacy securecookie format under the +// first legacy key pair, so pods that only understand the legacy format can +// read freshly minted cookies during a rolling deploy. Requires +// WithLegacyKeyPairs. This is stage 1 of the rollout; a follow-up removes the +// option, flipping encoding to the v1 format. JSON null values inside the +// payload are coerced to empty strings by the bridge; do not use pointer-typed +// fields while legacy encode is enabled. +func WithLegacyEncode() Option { + return func(c *config) { c.legacyEncode = true } +} + +// sealLegacy bridges T through its JSON representation into the flat +// string-to-string map that the securecookie stores used. +func (c *Codec[T]) sealLegacy(name string, value T) (string, error) { + buf, err := json.Marshal(value) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot marshal cookie value") + } + var flat map[string]string + if err := json.Unmarshal(buf, &flat); err != nil { + return "", errors.Wrap(err, "cookiex: payload must be a flat JSON object with string values while legacy encode is enabled") + } + if flat == nil { + return "", errors.New("cookiex: payload must be a flat JSON object with string values while legacy encode is enabled") + } + values := make(map[any]any, len(flat)) + for k, v := range flat { + values[k] = v + } + encoded, err := securecookie.EncodeMulti(name, values, c.legacy.codecs[0]) + if err != nil { + return "", errors.Wrap(err, "cookiex: cannot encode legacy cookie") + } + return encoded, nil +} diff --git a/oryx/corsx/check_origin.go b/oryx/corsx/check_origin.go new file mode 100644 index 0000000..8022dfb --- /dev/null +++ b/oryx/corsx/check_origin.go @@ -0,0 +1,141 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import ( + "strings" + + "golang.org/x/net/publicsuffix" +) + +// CheckOrigin is a function that can be used well with cors.Options.AllowOriginRequestFunc. +// It checks whether the origin is allowed following the same behavior as github.com/rs/cors. +// +// When legacyAllowInsecureOrigins is false (the default), wildcard patterns are +// only honored when ClassifyOrigin reports them as bounded at a registrable +// domain. Pass true to opt into legacy (trusting) behavior for unbounded +// wildcards. +// +// TODO: legacyAllowInsecureOrigins grandfathers a fixed set of projects through +// a time-boxed migration window (feature_flags.legacy_allow_insecure_origins). +// Once those projects move to bounded wildcards and the entitlement is revoked, +// drop this parameter and always enforce the boundary. +// +// Recommended usage for hot-reloadable origins: +// +// func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) { +// opts, enabled := p.GetProvider(ctx).CORS(prefix, cors.Options{ +// AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"}, +// AllowedHeaders: []string{"Authorization", "Content-Type", "Cookie"}, +// ExposedHeaders: []string{"Content-Type", "Set-Cookie"}, +// AllowCredentials: true, +// }) +// opts.AllowOriginRequestFunc = func(r *http.Request, origin string) bool { +// // load the origins from the config on every request to allow hot-reloading +// allowedOrigins := p.GetProvider(r.Context()).Strings(prefix + ".cors.allowed_origins") +// return corsx.CheckOrigin(allowedOrigins, origin, false) +// } +// return opts, enabled +// } +func CheckOrigin(allowedOrigins []string, origin string, legacyAllowInsecureOrigins bool) bool { + if len(allowedOrigins) == 0 { + return true + } + for _, o := range allowedOrigins { + if o == "*" { + // allow all origins + return true + } + // Note: for origins and methods matching, the spec requires a case-sensitive matching. + // As it may be error-prone, we chose to ignore the spec here. + // https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/cors.go#L132-L133 + o = strings.ToLower(o) + prefix, suffix, found := strings.Cut(o, "*") + if !found { + // not a pattern, check for equality + if o == origin { + return true + } + continue + } + // Only honor wildcards bounded at a registrable domain unless the caller + // explicitly opts into legacy insecure matching. See ClassifyOrigin. + if !legacyAllowInsecureOrigins && ClassifyOrigin(o).IsUnsafeWildcard() { + continue + } + // inspired by https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/utils.go#L15 + if len(origin) >= len(prefix)+len(suffix) && strings.HasPrefix(origin, prefix) && strings.HasSuffix(origin, suffix) { + return true + } + } + return false +} + +// OriginPattern describes a CORS origin or return-URL host pattern: whether it +// uses a wildcard and, if so, whether that wildcard is safely bounded at a +// registrable domain. +type OriginPattern struct { + // HasWildcard reports whether the pattern contains a "*". + HasWildcard bool + + // BoundedWildcard reports whether the "*" is confined to a subdomain label + // and the fixed domain that follows it is a registrable domain (an eTLD+1, + // e.g. "example.com" or "example.co.uk"). Every host the pattern can match + // then shares that one customer-owned registrable domain, so an attacker + // cannot register a matching host. Always false when HasWildcard is false. + BoundedWildcard bool + + // Base is the fixed domain that follows the wildcard label — "example.com" + // for "*.example.com", "com" for "*.com". It is empty for non-wildcards and + // for bare or trailing wildcards where no domain follows the "*". When + // BoundedWildcard is false, Base names the offending suffix, which is the + // actionable signal for reporting why a wildcard was rejected. + Base string +} + +// IsUnsafeWildcard reports whether the pattern is a wildcard that is NOT bounded +// at a registrable domain. Such a wildcard would match an attacker-registrable +// host (e.g. "https://*foo.com" matches "https://evilfoo.com"), so it must be +// rejected unless the caller explicitly opts into legacy insecure matching. This +// is the dominant question at call sites that gate, drop, or reject wildcard +// origins and return URLs. +func (p OriginPattern) IsUnsafeWildcard() bool { + return p.HasWildcard && !p.BoundedWildcard +} + +// ClassifyOrigin inspects a CORS origin or bare host pattern and reports whether +// it is a wildcard and, if so, whether the wildcard is safely bounded at a +// registrable domain. Only the text from the last "*" onward is inspected, so +// the result is identical whether pattern carries a scheme or is a bare host; a +// trailing ":port" is ignored. Examples: +// +// - "https://*.example.com" → {HasWildcard: true, BoundedWildcard: true, Base: "example.com"} +// - "https://*foo.com" → {HasWildcard: true, Base: "com"} (dot-less; base is a public suffix) +// - "https://*.com" → {HasWildcard: true, Base: "com"} (public suffix, not registrable) +// - "https://www.ory.*" → {HasWildcard: true} (trailing; no domain follows) +// - "https://exact.foo.com" → {} (no wildcard) +func ClassifyOrigin(pattern string) OriginPattern { + i := strings.LastIndexByte(pattern, '*') + if i < 0 { + return OriginPattern{} + } + p := OriginPattern{HasWildcard: true} + // The fixed domain is everything after the first "." that follows the last + // "*" — i.e. the label containing the wildcard is dropped. Without such a dot + // the wildcard is trailing or bare, so no registrable domain follows. + _, base, found := strings.Cut(pattern[i:], ".") + if !found { + return p + } + base = strings.ToLower(base) + if host, _, found := strings.Cut(base, ":"); found { + base = host // Drop a trailing ":port". + } + p.Base = base + // EffectiveTLDPlusOne returns an error when base is itself a public suffix + // (e.g. "com", "co.uk", "vercel.app") or otherwise has no registrable domain. + _, err := publicsuffix.EffectiveTLDPlusOne(base) + p.BoundedWildcard = err == nil + return p +} diff --git a/oryx/corsx/cmd.go b/oryx/corsx/cmd.go new file mode 100644 index 0000000..b475201 --- /dev/null +++ b/oryx/corsx/cmd.go @@ -0,0 +1,46 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +// HelpMessage returns a string containing information on setting up this CORS middleware. +func HelpMessage() string { + return `- CORS_ENABLED: Switch CORS support on (true) or off (false). Default is off (false). + + Example: CORS_ENABLED=true + +- CORS_ALLOWED_ORIGINS: A list of origins (comma separated values) a cross-domain request can be executed from. + If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) + to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. + Only one wildcard can be used per origin. The default value is *. + + Example: CORS_ALLOWED_ORIGINS=http://*.domain.com,http://*.domain2.com + +- CORS_ALLOWED_METHODS: A list of methods (comma separated values) the client is allowed to use with cross-domain + requests. Default value is simple methods (GET and POST). + + Example: CORS_ALLOWED_METHODS=POST,GET,PUT + +- CORS_ALLOWED_CREDENTIALS: Indicates whether the request can include user credentials like cookies, HTTP authentication + or client side SSL certificates. + + Default: CORS_ALLOWED_CREDENTIALS=false + Example: CORS_ALLOWED_CREDENTIALS=true + +- CORS_DEBUG: Debugging flag adds additional output to debug server side CORS issues. + + Default: CORS_DEBUG=false + Example: CORS_DEBUG=true + +- CORS_MAX_AGE: Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 + which stands for no max age. + + Default: CORS_MAX_AGE=0 + Example: CORS_MAX_AGE=10 + +- CORS_ALLOWED_HEADERS: A list of non simple headers (comma separated values) the client is allowed to use with + cross-domain requests. + +- CORS_EXPOSED_HEADERS: Indicates which headers (comma separated values) are safe to expose to the API of a + CORS API specification.` +} diff --git a/oryx/corsx/defaults.go b/oryx/corsx/defaults.go new file mode 100644 index 0000000..136bdec --- /dev/null +++ b/oryx/corsx/defaults.go @@ -0,0 +1,33 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +// CORSRequestHeadersSafelist We add the safe list cors accept headers +// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header +var CORSRequestHeadersSafelist = []string{"Accept", "Content-Type", "Content-Length", "Accept-Language", "Content-Language"} + +// CORSResponseHeadersSafelist We add the safe list cors expose headers +// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header +var CORSResponseHeadersSafelist = []string{"Set-Cookie", "Cache-Control", "Expires", "Last-Modified", "Pragma", "Content-Length", "Content-Language", "Content-Type"} + +// CORSDefaultAllowedMethods Default allowed methods +var CORSDefaultAllowedMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE"} + +// CORSRequestHeadersExtended Extended list of request headers +// these will be concatenated with the safelist +var CORSRequestHeadersExtended = []string{"Authorization", "X-CSRF-TOKEN"} + +// CORSResponseHeadersExtended Extended list of response headers +// these will be concatenated with the safelist +var CORSResponseHeadersExtended = []string{} + +// CORSDefaultMaxAge max age for cache of preflight request result +// default is 5 seconds +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age +var CORSDefaultMaxAge = 5 + +// CORSAllowCredentials default value for allow credentials +// this is required for cookies to be sent by the browser +// we always want this since we are using cookies for authentication most of the time +var CORSAllowCredentials = true diff --git a/oryx/corsx/middleware.go b/oryx/corsx/middleware.go new file mode 100644 index 0000000..a6ab0b8 --- /dev/null +++ b/oryx/corsx/middleware.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import ( + "context" + "net/http" + + "github.com/rs/cors" + "github.com/urfave/negroni" +) + +// ContextualizedMiddleware is a context-aware CORS middleware. It allows hot-reloading CORS configuration using +// the HTTP request context. +// +// n := negroni.New() +// n.UseFunc(ContextualizedMiddleware(func(context.Context) (opts cors.Options, enabled bool) { +// panic("implement me") +// }) +// // ... +// +// Deprecated: because this is not really practical to use, you should use CheckOrigin as the cors.Options.AllowOriginRequestFunc instead. +func ContextualizedMiddleware(provider func(context.Context) (opts cors.Options, enabled bool)) negroni.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + options, enabled := provider(r.Context()) + if !enabled { + next(rw, r) + return + } + + cors.New(options).Handler(next).ServeHTTP(rw, r) + } +} diff --git a/oryx/corsx/normalize.go b/oryx/corsx/normalize.go new file mode 100644 index 0000000..609b124 --- /dev/null +++ b/oryx/corsx/normalize.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package corsx + +import "net/url" + +// NormalizeOrigins normalizes the CORS origins. +func NormalizeOrigins(origins []url.URL) []string { + results := make([]string, len(origins)) + for k, o := range origins { + results[k] = o.Scheme + "://" + o.Host + } + return results +} + +// NormalizeOriginStrings normalizes the CORS origins from string representation +func NormalizeOriginStrings(origins []string) ([]string, error) { + results := make([]string, len(origins)) + for k, o := range origins { + u, err := url.ParseRequestURI(o) + if err != nil { + return nil, err + } + results[k] = u.Scheme + "://" + u.Host + } + return results, nil +} diff --git a/oryx/crdbx/isolation.go b/oryx/crdbx/isolation.go new file mode 100644 index 0000000..f714ecd --- /dev/null +++ b/oryx/crdbx/isolation.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" + "github.com/ory/x/sqlcon" +) + +// SetTransactionReadCommitted lowers the isolation of the current transaction +// to READ COMMITTED on CockroachDB. Multi-statement read transactions at +// SERIALIZABLE must refresh their whole read set when a concurrent write +// bumps the transaction timestamp, and surface RETRY_SERIALIZABLE to the +// client when the refresh fails; under READ COMMITTED every statement reads +// at its own timestamp and those conflicts are absorbed server-side. Must be +// called before the transaction runs its first query. Postgres already runs +// statements at READ COMMITTED by default, and MySQL's SET TRANSACTION +// applies to the next transaction rather than the current one, so every +// other dialect is a no-op. YugabyteDB maps READ COMMITTED to snapshot +// isolation unless yb_enable_read_committed_isolation is enabled, so it stays +// on the normal transaction retry path instead of using this CockroachDB-only +// override. +func SetTransactionReadCommitted(c *pop.Connection) error { + if c.Dialect.Name() != dbal.DriverCockroachDB { + // Only CockroachDB supports and needs this. + return nil + } + + return sqlcon.HandleError(c.RawQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED").Exec()) +} diff --git a/oryx/crdbx/readonly.go b/oryx/crdbx/readonly.go new file mode 100644 index 0000000..595060a --- /dev/null +++ b/oryx/crdbx/readonly.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" + "github.com/ory/x/sqlcon" +) + +// SetTransactionReadOnly sets the current transaction to read only for +// CockroachDB, PostgreSQL, and YugabyteDB, which all let the server skip +// write-path bookkeeping and reject accidental writes. Must be called before the +// transaction runs its first query. MySQL's SET TRANSACTION applies to the +// next transaction rather than the current one, and SQLite has no +// equivalent, so every other dialect is a no-op. +func SetTransactionReadOnly(c *pop.Connection) error { + if dbal.IsPostgresCompatible(c.Dialect.Name()) { + return sqlcon.HandleError(c.RawQuery("SET TRANSACTION READ ONLY").Exec()) + } + return nil +} diff --git a/oryx/crdbx/staleness.go b/oryx/crdbx/staleness.go new file mode 100644 index 0000000..f915884 --- /dev/null +++ b/oryx/crdbx/staleness.go @@ -0,0 +1,110 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package crdbx + +import ( + "net/http" + + "github.com/ory/x/dbal" + + "github.com/ory/pop/v6" + + "github.com/ory/x/sqlcon" +) + +// Control API consistency guarantees +// +// swagger:model consistencyRequestParameters +type ConsistencyRequestParameters struct { + // Read Consistency Level (preview) + // + // The read consistency level determines the consistency guarantee for reads: + // + // - strong (slow): The read is guaranteed to return the most recent data committed at the start of the read. + // - eventual (very fast): The result will return data that is about 4.8 seconds old. + // + // The default consistency guarantee can be changed in the Ory Network Console or using the Ory CLI with + // `ory patch project --replace '/previews/default_read_consistency_level="strong"'`. + // + // Setting the default consistency level to `eventual` may cause regressions in the future as we add consistency + // controls to more APIs. Currently, the following APIs will be affected by this setting: + // + // - `GET /admin/identities` + // + // This feature is in preview and only available in Ory Network. + // + // required: false + // in: query + Consistency ConsistencyLevel `json:"consistency"` +} + +// ConsistencyLevel is the consistency level. +// swagger:enum ConsistencyLevel +type ConsistencyLevel string + +const ( + // ConsistencyLevelUnset is the unset / default consistency level. + ConsistencyLevelUnset ConsistencyLevel = "" + // ConsistencyLevelStrong is the strong consistency level. + ConsistencyLevelStrong ConsistencyLevel = "strong" + // ConsistencyLevelEventual is the eventual consistency level using follower read timestamps. + ConsistencyLevelEventual ConsistencyLevel = "eventual" +) + +// ConsistencyLevelFromRequest extracts the consistency level from a request. +func ConsistencyLevelFromRequest(r *http.Request) ConsistencyLevel { + return ConsistencyLevelFromString(r.URL.Query().Get("consistency")) +} + +// ConsistencyLevelFromString converts a string to a ConsistencyLevel. +// If the string is not recognized or unset, ConsistencyLevelStrong is returned. +func ConsistencyLevelFromString(in string) ConsistencyLevel { + switch in { + case string(ConsistencyLevelStrong): + return ConsistencyLevelStrong + case string(ConsistencyLevelEventual): + return ConsistencyLevelEventual + case string(ConsistencyLevelUnset): + return ConsistencyLevelUnset + } + return ConsistencyLevelStrong +} + +// SetTransactionConsistency sets the transaction consistency level for CockroachDB. +func SetTransactionConsistency(c *pop.Connection, level ConsistencyLevel, fallback ConsistencyLevel) error { + q := getTransactionConsistencyQuery(c.Dialect.Name(), level, fallback) + if len(q) == 0 { + return nil + } + + return sqlcon.HandleError(c.RawQuery(q).Exec()) +} + +const transactionFollowerReadTimestamp = "SET TRANSACTION AS OF SYSTEM TIME follower_read_timestamp()" + +func getTransactionConsistencyQuery(dialect string, level ConsistencyLevel, fallback ConsistencyLevel) string { + if dialect != dbal.DriverCockroachDB { + // Only CockroachDB supports this. + return "" + } + + switch level { + case ConsistencyLevelStrong: + // Nothing to do + return "" + case ConsistencyLevelEventual: + // Jumps to end of function + case ConsistencyLevelUnset: + fallthrough + default: + if fallback != ConsistencyLevelEventual { + // Nothing to do + return "" + } + + // Jumps to end of function + } + + return transactionFollowerReadTimestamp +} diff --git a/oryx/dbal/canonicalize.go b/oryx/dbal/canonicalize.go new file mode 100644 index 0000000..672fada --- /dev/null +++ b/oryx/dbal/canonicalize.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +const ( + DriverMySQL = "mysql" + DriverCockroachDB = "cockroach" + DriverPostgreSQL = "postgres" + // DriverYugabyteDB is the dialect name YugabyteDB connections report. It is + // a commercial (Ory Enterprise License) dialect that speaks the PostgreSQL + // wire protocol and SQL dialect, so most SQL paths treat it like postgres. + DriverYugabyteDB = "yugabyte" +) + +// IsPostgresCompatible reports whether dialect speaks the PostgreSQL SQL +// dialect. It does not imply support for CockroachDB-specific syntax or +// features. +func IsPostgresCompatible(dialect string) bool { + switch dialect { + case DriverPostgreSQL, DriverCockroachDB, DriverYugabyteDB: + return true + default: + return false + } +} diff --git a/oryx/dbal/driver.go b/oryx/dbal/driver.go new file mode 100644 index 0000000..ae6ed33 --- /dev/null +++ b/oryx/dbal/driver.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "strings" +) + +// IsSQLite returns true if the connection is a SQLite string. +func IsSQLite(dsn string) bool { + scheme := strings.Split(dsn, "://")[0] + return scheme == "sqlite" || scheme == "sqlite3" +} diff --git a/oryx/dbal/dsn.go b/oryx/dbal/dsn.go new file mode 100644 index 0000000..d9c3c85 --- /dev/null +++ b/oryx/dbal/dsn.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/stretchr/testify/require" +) + +var sqliteMemoryRegexp = regexp.MustCompile(`^sqlite://file:.+\?.*&?mode=memory($|&.*)|sqlite://(file:)?:memory:\?.*$|^(:memory:|memory)$`) + +// IsMemorySQLite returns true if a given DSN string is pointing to a SQLite database. +// +// SQLite can be written in different styles depending on the use case +// - just in memory +// - shared connection +// - shared but unique in the same process +// see: https://sqlite.org/inmemorydb.html +func IsMemorySQLite(dsn string) bool { return sqliteMemoryRegexp.MatchString(dsn) } + +// NewSQLiteTestDatabase creates a new, unique SQLite database +// which is shared amongst all callers and identified by an individual file name. +// The database file is created in the system's temporary directory, and not actively +// removed to allow debugging in case of test failures. +func NewSQLiteTestDatabase(t testing.TB) string { + fn, err := os.MkdirTemp("", "sqlite-test-db-*") + require.NoError(t, err) + return NewSQLiteDatabase(fn) +} + +// NewSQLiteDatabase creates a new on-disk, unique SQLite database +// which is shared amongst all callers and identified by an individual file name. +// This is sometimes necessary over a in-memory database, for example when multiple tests/goroutines run in parallel +// and access the same table. +// This would result in a locking error from SQLite when running in-memory. +// Additionally, shared cache mode is deprecated and discouraged, and the problem is better solved with the WAL, +// according to official docs. +// +// In tests, use [NewSQLiteTestDatabase] instead. +func NewSQLiteDatabase(name string) string { + return fmt.Sprintf("sqlite://%s/db.sqlite?_fk=true&_busy_timeout=100000&_pragma=journal_mode(WAL)&_time_format=sqlite", name) +} diff --git a/oryx/dbal/testhelpers.go b/oryx/dbal/testhelpers.go new file mode 100644 index 0000000..91df8af --- /dev/null +++ b/oryx/dbal/testhelpers.go @@ -0,0 +1,62 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dbal + +import ( + "bytes" + "encoding/hex" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + "github.com/ory/x/fsx" + "github.com/ory/x/sqlcon/dockertest" +) + +var hashDumpRegex = regexp.MustCompile(`-- migrations hash: ([^\n]+)\n`) + +func RestoreFromSchemaDump(t testing.TB, c *pop.Connection, migrations fs.FS, writeTo string) func(testing.TB) { + newHash, err := fsx.DirHash(migrations) + require.NoError(t, err) + + dumpFilename := filepath.Join(writeTo, c.Dialect.Name()+"_dump.sql") + + updateDump := func(t testing.TB) { + dump := dockertest.DumpSchema(t, c) + f, err := os.OpenFile(dumpFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + require.NoError(t, err) + defer f.Close() + + _, _ = fmt.Fprintf(f, "-- migrations hash: %x\n\n%s", newHash, dump) + t.Fatal("database schema restored from migrations and dump updated, please re-run the test") + } + + dump, err := os.ReadFile(dumpFilename) + if errors.Is(err, fs.ErrNotExist) { + return updateDump + } + require.NoError(t, err) + + matches := hashDumpRegex.FindSubmatch(dump) + if len(matches) != 2 { + return updateDump + } + + currentHash, err := hex.DecodeString(string(matches[1])) + require.NoError(t, err) + + if !bytes.Equal(newHash, currentHash) { + return updateDump + } + + require.NoError(t, c.RawQuery(string(dump)).Exec()) + return nil +} diff --git a/oryx/decoderx/http.go b/oryx/decoderx/http.go new file mode 100644 index 0000000..384dc9b --- /dev/null +++ b/oryx/decoderx/http.go @@ -0,0 +1,561 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package decoderx + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/jsonschema/v3" + + "github.com/ory/herodot" + + "github.com/ory/x/httpx" + "github.com/ory/x/jsonschemax" +) + +type ( + httpDecoderOptions struct { + keepRequestBody bool + allowedContentTypes []string + allowedHTTPMethods []string + jsonSchemaRef string + jsonSchemaCompiler *jsonschema.Compiler + jsonSchemaValidate bool + maxCircularReferenceDepth uint8 + handleParseErrors parseErrorStrategy + expectJSONFlattened bool + queryAndBody bool + } + + // HTTPDecoderOption configures the HTTP decoder. + HTTPDecoderOption func(*httpDecoderOptions) + + parseErrorStrategy uint8 +) + +const ( + httpContentTypeMultipartForm = "multipart/form-data" + httpContentTypeURLEncodedForm = "application/x-www-form-urlencoded" + httpContentTypeJSON = "application/json" +) + +const ( + // ParseErrorIgnoreConversionErrors will ignore any errors caused by strconv.Parse* and use the + // raw form field value, which is a string, when such a parse error occurs. + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field + // `ratio` will be handled as a string. If the destination struct is a `json.RawMessage`, then + // the output will be `{"ratio": "foobar"}`. + ParseErrorIgnoreConversionErrors parseErrorStrategy = iota + 1 + + // ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the + // default value of the type to be casted, e.g. float64(0), string(""). + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` then field + // `ratio` will receive the default value for the primitive type (here `0.0` for `number`). + // If the destination struct is a `json.RawMessage`, then the output will be `{"ratio": 0.0}`. + ParseErrorUseEmptyValueOnConversionErrors + + // ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns + // an error. + // + // If the JSON Schema defines `{"ratio": {"type": "number"}}` but `ratio=foobar` the parser aborts + // and returns an error, here: `strconv.ParseFloat: parsing "foobar"`. + ParseErrorReturnOnConversionErrors +) + +var errKeyNotFound = errors.New("key not found") + +// HTTPFormDecoder configures the HTTP decoder to only accept form-data +// (application/x-www-form-urlencoded, multipart/form-data) +func HTTPFormDecoder() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedContentTypes = []string{httpContentTypeMultipartForm, httpContentTypeURLEncodedForm} + } +} + +// HTTPJSONDecoder configures the HTTP decoder to only accept JSON data +// (application/json). +func HTTPJSONDecoder() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedContentTypes = []string{httpContentTypeJSON} + } +} + +// HTTPKeepRequestBody configures the HTTP decoder to allow other +// HTTP request body readers to read the body as well by keeping +// the data in memory. +func HTTPKeepRequestBody(keep bool) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.keepRequestBody = keep + } +} + +// HTTPDecoderSetValidatePayloads sets if payloads should be validated or not. +func HTTPDecoderSetValidatePayloads(validate bool) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.jsonSchemaValidate = validate + o.keepRequestBody = true + } +} + +// HTTPDecoderJSONFollowsFormFormat if set tells the decoder that JSON follows the same conventions +// as the form decoder, meaning `{"foo.bar": "..."}` is translated to `{"foo": {"bar": "..."}}`. +func HTTPDecoderJSONFollowsFormFormat() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.expectJSONFlattened = true + o.keepRequestBody = true + } +} + +// HTTPDecoderAllowedMethods sets the allowed HTTP methods. Defaults are POST, PUT, PATCH. +func HTTPDecoderAllowedMethods(method ...string) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.allowedHTTPMethods = method + } +} + +// HTTPDecoderUseQueryAndBody will check both the HTTP body and the HTTP query params when decoding. +// Only relevant for non-GET operations. +func HTTPDecoderUseQueryAndBody() HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.queryAndBody = true + } +} + +// HTTPDecoderSetIgnoreParseErrorsStrategy sets a strategy for dealing with strconv.Parse* errors: +// +// - decoderx.ParseErrorIgnoreConversionErrors will ignore any parse errors caused by strconv.Parse* and use the +// raw form field value, which is a string, when such a parse error occurs. (default) +// - decoderx.ParseErrorUseEmptyValueOnConversionErrors will ignore any parse errors caused by strconv.Parse* and use the +// default value of the type to be casted, e.g. float64(0), string(""). +// - decoderx.ParseErrorReturnOnConversionErrors will abort and return with an error if strconv.Parse* returns +// an error. +func HTTPDecoderSetIgnoreParseErrorsStrategy(strategy parseErrorStrategy) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.handleParseErrors = strategy + } +} + +// HTTPDecoderSetMaxCircularReferenceDepth sets the maximum recursive reference resolution depth. +func HTTPDecoderSetMaxCircularReferenceDepth(depth uint8) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + o.maxCircularReferenceDepth = depth + } +} + +// HTTPJSONSchemaCompiler sets a JSON schema to be used for validation and type assertion of +// incoming requests. +func HTTPJSONSchemaCompiler(ref string, compiler *jsonschema.Compiler) HTTPDecoderOption { + return func(o *httpDecoderOptions) { + if compiler == nil { + compiler = jsonschema.NewCompiler() + } + compiler.ExtractAnnotations = true + o.jsonSchemaCompiler = compiler + o.jsonSchemaRef = ref + o.jsonSchemaValidate = true + } +} + +// HTTPRawJSONSchemaCompiler uses a JSON Schema Compiler with the provided JSON Schema in raw byte form. +func HTTPRawJSONSchemaCompiler(raw []byte) (HTTPDecoderOption, error) { + compiler := jsonschema.NewCompiler() + id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) + if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { + return nil, err + } + compiler.ExtractAnnotations = true + + return func(o *httpDecoderOptions) { + o.jsonSchemaCompiler = compiler + o.jsonSchemaRef = id + o.jsonSchemaValidate = true + }, nil +} + +// MustHTTPRawJSONSchemaCompiler uses HTTPRawJSONSchemaCompiler and panics on error. +func MustHTTPRawJSONSchemaCompiler(raw []byte) HTTPDecoderOption { + f, err := HTTPRawJSONSchemaCompiler(raw) + if err != nil { + panic(err) + } + return f +} + +func newHTTPDecoderOptions(fs []HTTPDecoderOption) *httpDecoderOptions { + o := &httpDecoderOptions{ + allowedContentTypes: []string{ + httpContentTypeMultipartForm, httpContentTypeURLEncodedForm, httpContentTypeJSON, + }, + allowedHTTPMethods: []string{"POST", "PUT", "PATCH"}, + maxCircularReferenceDepth: 5, + handleParseErrors: ParseErrorIgnoreConversionErrors, + } + + for _, f := range fs { + f(o) + } + + return o +} + +func validateRequest(r *http.Request, c *httpDecoderOptions) error { + method := strings.ToUpper(r.Method) + + if !slices.Contains(c.allowedHTTPMethods, method) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to decode body because HTTP Request Method was "%s" but only %v are supported.`, method, c.allowedHTTPMethods)) + } + + if method != "GET" { + if r.ContentLength == 0 { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`Unable to decode HTTP Request Body because its HTTP Header "Content-Length" is zero.`)) + } + + if !httpx.HasContentType(r, c.allowedContentTypes...) { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf(`HTTP %s Request used unknown HTTP Header "Content-Type: %s", only %v are supported.`, method, r.Header.Get("Content-Type"), c.allowedContentTypes)) + } + } + + return nil +} + +func validatePayload(ctx context.Context, raw json.RawMessage, c *httpDecoderOptions) error { + if !c.jsonSchemaValidate { + return nil + } + + if c.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("JSON Schema Validation is required but no compiler was provided.")) + } + + schema, err := c.jsonSchemaCompiler.Compile(ctx, c.jsonSchemaRef) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to load JSON Schema from location: %s", c.jsonSchemaRef).WithDebug(err.Error())) + } + + if err := schema.Validate(bytes.NewBuffer(raw)); err != nil { + if errors.As(err, new(*jsonschema.ValidationError)) { + return errors.WithStack(err) + } + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to process JSON Schema and input: %s", err).WithDebug(err.Error())) + } + + return nil +} + +// Decode takes an HTTP Request Body and decodes it into destination. +func Decode(r *http.Request, destination any, opts ...HTTPDecoderOption) error { + c := newHTTPDecoderOptions(opts) + if err := validateRequest(r, c); err != nil { + return err + } + + if r.Method == "GET" { + return decodeForm(r, destination, c) + } else if httpx.HasContentType(r, httpContentTypeJSON) { + if c.expectJSONFlattened { + return decodeJSONForm(r, destination, c) + } + return decodeJSON(r, destination, c) + } else if httpx.HasContentType(r, httpContentTypeMultipartForm, httpContentTypeURLEncodedForm) { + return decodeForm(r, destination, c) + } + + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to determine decoder for content type: %s", r.Header.Get("Content-Type"))) +} + +func requestBody(r *http.Request, o *httpDecoderOptions) (reader io.ReadCloser, err error) { + if strings.ToUpper(r.Method) == "GET" { + return io.NopCloser(bytes.NewBufferString(r.URL.Query().Encode())), nil + } + + if !o.keepRequestBody { + return r.Body, nil + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + return nil, errors.Wrapf(err, "unable to read body") + } + + _ = r.Body.Close() // must close + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + return io.NopCloser(bytes.NewBuffer(bodyBytes)), nil +} + +func decodeJSONForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + if o.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) + } + + paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) + } + + reader, err := requestBody(r, o) + if err != nil { + return err + } + + var interim json.RawMessage + if err := json.NewDecoder(reader).Decode(&interim); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()).WithReason("Unable to decode form as JSON.")) + } + + parsed := gjson.ParseBytes(interim) + if !parsed.IsObject() { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected JSON sent in request body to be an object but got: %s", parsed.Type.String())) + } + + values := url.Values{} + parsed.ForEach(func(k, v gjson.Result) bool { + values.Set(k.String(), v.String()) + return true + }) + + if o.queryAndBody { + _ = r.ParseForm() + for k := range r.Form { + values.Set(k, r.Form.Get(k)) + } + } + + raw, err := decodeURLValues(values, paths, o) + if err != nil { + return err + } + + if err := json.Unmarshal(raw, destination); err != nil { + return errors.WithStack(err) + } + + return validatePayload(r.Context(), raw, o) +} + +func decodeForm(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + if o.jsonSchemaCompiler == nil { + return errors.WithStack(herodot.ErrInternalServerError().WithReasonf("Unable to decode HTTP Form Body because no validation schema was provided. This is a code bug.")) + } + + reader, err := requestBody(r, o) + if err != nil { + return err + } + + defer func() { + r.Body = reader + }() + + if err := r.ParseForm(); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode HTTP %s form body: %s", strings.ToUpper(r.Method), err).WithDebug(err.Error())) + } + + paths, err := jsonschemax.ListPathsWithRecursion(r.Context(), o.jsonSchemaRef, o.jsonSchemaCompiler, o.maxCircularReferenceDepth) + if err != nil { + return errors.WithStack(herodot.ErrInternalServerError().WithTrace(err).WithReasonf("Unable to prepare JSON Schema for HTTP Post Body Form parsing: %s", err).WithDebugf("%+v", err)) + } + + values := r.PostForm + if r.Method == "GET" || o.queryAndBody { + values = r.Form + } + + raw, err := decodeURLValues(values, paths, o) + if err != nil && !errors.Is(err, errKeyNotFound) { + return err + } + + if err := json.NewDecoder(bytes.NewReader(raw)).Decode(destination); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode JSON payload: %s", err)) + } + + return validatePayload(r.Context(), raw, o) +} + +func decodeURLValues(values url.Values, paths []jsonschemax.Path, o *httpDecoderOptions) (json.RawMessage, error) { + raw := json.RawMessage(`{}`) + for key := range values { + for _, path := range paths { + if key == path.Name { + var err error + switch path.Type.(type) { + case []string: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + case []float64: + for k, v := range values[key] { + var f float64 + if f, err = strconv.ParseFloat(v, 64); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a number."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetailf("index", "%d", k). + WithDetail("value", v)) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), f) + } + } + case []bool: + for k, v := range values[key] { + var b bool + if b, err = strconv.ParseBool(v); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), b) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a boolean."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetailf("index", "%d", k). + WithDetail("value", v)) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name+"."+strconv.Itoa(k), b) + } + } + case []interface{}: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + case bool: + v := values[key][len(values[key])-1] + if len(v) == 0 { + if !path.Required { + continue + } + v = "false" + } + + var b bool + if b, err = strconv.ParseBool(v); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, b) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a boolean."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetail("value", values.Get(key))) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name, b) + } + case float64: + v := values.Get(key) + if len(v) == 0 { + if !path.Required { + continue + } + v = "0.0" + } + + var f float64 + if f, err = strconv.ParseFloat(v, 64); err != nil { + switch o.handleParseErrors { + case ParseErrorIgnoreConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, v) + case ParseErrorUseEmptyValueOnConversionErrors: + raw, err = sjson.SetBytes(raw, path.Name, f) + case ParseErrorReturnOnConversionErrors: + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Expected value to be a number."). + WithDetail("parse_error", err.Error()). + WithDetail("name", key). + WithDetail("value", values.Get(key))) + } + } else { + raw, err = sjson.SetBytes(raw, path.Name, f) + } + case string: + v := values.Get(key) + if len(v) == 0 { + continue + } + + raw, err = sjson.SetBytes(raw, path.Name, v) + case map[string]interface{}: + v := values.Get(key) + if len(v) == 0 && !path.Required { + continue + } + + raw, err = sjson.SetRawBytes(raw, path.Name, []byte(v)) + case []map[string]interface{}: + raw, err = sjson.SetBytes(raw, path.Name, values[key]) + } + + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to type assert values from HTTP Post Body: %s", err)) + } + break + } + } + } + + for _, path := range paths { + if path.TypeHint != jsonschemax.JSON { + continue + } + + if !gjson.GetBytes(raw, path.Name).Exists() { + var err error + raw, err = sjson.SetRawBytes(raw, path.Name, []byte(`{}`)) + if err != nil { + return nil, errors.WithStack(err) + } + } + } + + return raw, nil +} + +func decodeJSON(r *http.Request, destination interface{}, o *httpDecoderOptions) error { + reader, err := requestBody(r, o) + if err != nil { + return err + } + + raw, err := io.ReadAll(reader) + if err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to read HTTP POST body: %s", err)) + } + + dc := json.NewDecoder(bytes.NewReader(raw)) + if err := dc.Decode(destination); err != nil { + return errors.WithStack(herodot.ErrBadRequest().WithReasonf("Unable to decode JSON payload: %s", err).WithDebugf("Received request body: %s", string(raw))) + } + + if err := validatePayload(r.Context(), raw, o); err != nil { + if o.expectJSONFlattened && strings.Contains(err.Error(), "json: unknown field") { + return decodeJSONForm(r, destination, o) + } + return err + } + + return nil +} diff --git a/oryx/decoderx/stub/consent.json b/oryx/decoderx/stub/consent.json new file mode 100644 index 0000000..6539260 --- /dev/null +++ b/oryx/decoderx/stub/consent.json @@ -0,0 +1,53 @@ +{ + "$id": "https://example.com/ory.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "traits": { + "additionalProperties": false, + "properties": { + "consent": { + "additionalProperties": false, + "properties": { + "tos": { + "description": "yyyymmdd of when this was accepted", + "title": "I accept the Terms of Service https://www.ory.sh/ptos", + "const": true, + "maxLength": 30 + }, + "inner": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + }, + "required": ["foo"] + } + }, + "required": ["tos"], + "title": "Consent", + "type": "object" + }, + "notrequired": { + "additionalProperties": false, + "properties": { + "tos": { + "description": "yyyymmdd of when this was accepted", + "title": "I accept the Terms of Service https://www.ory.sh/ptos", + "const": true, + "maxLength": 30 + } + }, + "required": ["tos"], + "title": "Consent", + "type": "object" + } + }, + "required": ["consent"], + "type": "object" + } + }, + "title": "Person", + "type": "object" +} diff --git a/oryx/decoderx/stub/dynamic-object.json b/oryx/decoderx/stub/dynamic-object.json new file mode 100644 index 0000000..beed127 --- /dev/null +++ b/oryx/decoderx/stub/dynamic-object.json @@ -0,0 +1,22 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + } + }, + "dynamic_object": { + "type": "object", + "additionalProperties": true + } + } +} diff --git a/oryx/decoderx/stub/nested.json b/oryx/decoderx/stub/nested.json new file mode 100644 index 0000000..4efa3af --- /dev/null +++ b/oryx/decoderx/stub/nested.json @@ -0,0 +1,36 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "required": ["node"], + "properties": { + "node": { + "type": "object", + "required": ["node"], + "properties": { + "node": { + "type": "object", + "properties": { + "node": { + "type": "object", + "properties": { + "leaf": { + "type": "string" + } + }, + "required": ["leaf"] + }, + "leaf": { + "type": "string" + } + }, + "required": ["leaf"] + }, + "leaf": { + "type": "string" + } + } + } + } +} diff --git a/oryx/decoderx/stub/person.json b/oryx/decoderx/stub/person.json new file mode 100644 index 0000000..7779aac --- /dev/null +++ b/oryx/decoderx/stub/person.json @@ -0,0 +1,31 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + } + }, + "age": { + "type": "integer" + }, + "ratio": { + "type": "number" + }, + "consent": { + "type": "boolean" + }, + "newsletter": { + "type": "boolean" + } + } +} diff --git a/oryx/decoderx/stub/required-defaults.json b/oryx/decoderx/stub/required-defaults.json new file mode 100644 index 0000000..62edd80 --- /dev/null +++ b/oryx/decoderx/stub/required-defaults.json @@ -0,0 +1,57 @@ +{ + "$id": "https://example.com/person.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + }, + "required": ["first"] + }, + "name2": { + "type": "object", + "properties": { + "first": { + "type": "string" + }, + "last": { + "type": "string" + } + }, + "required": ["first"] + }, + "age": { + "type": "integer" + }, + "age2": { + "type": "integer" + }, + "ratio": { + "type": "number" + }, + "ratio2": { + "type": "number" + }, + "consent": { + "type": "boolean" + }, + "consent2": { + "type": "boolean" + }, + "newsletter": { + "type": "boolean" + }, + "newsletter2": { + "type": "boolean" + } + }, + "required": ["age2", "ratio2", "consent2", "newsletter2", "name2"] +} diff --git a/oryx/decoderx/stub/schema.json b/oryx/decoderx/stub/schema.json new file mode 100644 index 0000000..c748fbd --- /dev/null +++ b/oryx/decoderx/stub/schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["foo"], + "properties": { + "foo": { + "type": "string" + } + } +} diff --git a/oryx/docs/alpha_num.png b/oryx/docs/alpha_num.png new file mode 100644 index 0000000..2bbc656 Binary files /dev/null and b/oryx/docs/alpha_num.png differ diff --git a/oryx/docs/num.png b/oryx/docs/num.png new file mode 100644 index 0000000..ecaadf6 Binary files /dev/null and b/oryx/docs/num.png differ diff --git a/oryx/docs/result_num.png b/oryx/docs/result_num.png new file mode 100644 index 0000000..98bb043 Binary files /dev/null and b/oryx/docs/result_num.png differ diff --git a/oryx/errorsx/errors.go b/oryx/errorsx/errors.go new file mode 100644 index 0000000..801141c --- /dev/null +++ b/oryx/errorsx/errors.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package errorsx + +import ( + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +// Deprecated: you should probably use errors.As instead. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok || cause.Cause() == nil { + break + } + err = cause.Cause() + } + return err +} + +// WithStack mirror pkg/errors.WithStack but does not wrap existing stack +// traces. +// Deprecated: you should probably use errors.WithStack instead and only annotate stacks when it makes sense. +func WithStack(err error) error { + if e, ok := err.(StackTracer); ok && len(e.StackTrace()) > 0 { + return err + } + + return errors.WithStack(err) +} + +// StatusCodeCarrier can be implemented by an error to support setting status codes in the error itself. +type StatusCodeCarrier interface { + // StatusCode returns the status code of this error. + StatusCode() int +} + +// RequestIDCarrier can be implemented by an error to support error contexts. +type RequestIDCarrier interface { + // RequestID returns the ID of the request that caused the error, if applicable. + RequestID() string +} + +// ReasonCarrier can be implemented by an error to support error contexts. +type ReasonCarrier interface { + // Reason returns the reason for the error, if applicable. + Reason() string +} + +// DebugCarrier can be implemented by an error to support error contexts. +type DebugCarrier interface { + // Debug returns debugging information for the error, if applicable. + Debug() string +} + +// StatusCarrier can be implemented by an error to support error contexts. +type StatusCarrier interface { + // ID returns the error id, if applicable. + Status() string +} + +// DetailsCarrier can be implemented by an error to support error contexts. +type DetailsCarrier interface { + // Details returns details on the error, if applicable. + Details() map[string]interface{} +} + +// IDCarrier can be implemented by an error to support error contexts. +type IDCarrier interface { + // ID returns application error ID on the error, if applicable. + ID() string +} + +type StackTracer interface { + StackTrace() errors.StackTrace +} + +func GetCodeFromHerodotError(err error) (code int, ok bool) { + herodotErr := &herodot.DefaultError{} + isHerodot := errors.As(err, &herodotErr) + + return herodotErr.CodeField, isHerodot +} diff --git a/oryx/fetcher/fetcher.go b/oryx/fetcher/fetcher.go new file mode 100644 index 0000000..78195ef --- /dev/null +++ b/oryx/fetcher/fetcher.go @@ -0,0 +1,187 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fetcher + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/base64" + stderrors "errors" + "fmt" + "io" + "net/http" + "os" + "slices" + "strings" + "time" + + "github.com/dgraph-io/ristretto/v2" + "github.com/hashicorp/go-retryablehttp" + "github.com/pkg/errors" + + "github.com/ory/x/httpx" +) + +// Fetcher is able to load file contents from http, https, file, and base64 locations. +type Fetcher struct { + hc *retryablehttp.Client + limit int64 + cache *ristretto.Cache[[]byte, []byte] + ttl time.Duration + schemes []string +} + +type opts struct { + hc *retryablehttp.Client + limit int64 + cache *ristretto.Cache[[]byte, []byte] + ttl time.Duration + schemes []string +} + +var ErrUnknownScheme = stderrors.New("unknown scheme") + +// WithClient sets the http.Client the fetcher uses. +func WithClient(hc *retryablehttp.Client) Modifier { + return func(o *opts) { + o.hc = hc + } +} + +// WithMaxHTTPMaxBytes reads at most limit bytes from the HTTP response body, +// returning bytes.ErrToLarge if the limit would be exceeded. +func WithMaxHTTPMaxBytes(limit int64) Modifier { + return func(o *opts) { + o.limit = limit + } +} + +func WithCache(cache *ristretto.Cache[[]byte, []byte], ttl time.Duration) Modifier { + return func(o *opts) { + if ttl < 0 { + return + } + o.cache = cache + o.ttl = ttl + } +} + +func WithAllowedSchemes(schemes ...string) Modifier { + return func(o *opts) { + o.schemes = slices.Clone(schemes) + } +} + +func newOpts() *opts { + return &opts{ + hc: httpx.NewResilientClient(), + schemes: []string{"http", "https", "file", "base64"}, + } +} + +type Modifier func(*opts) + +// redactedSource returns a safe representation of the source for use in error +// messages. base64:// sources embed the full payload (which may contain +// secrets such as private keys) and are therefore redacted; all other sources +// are returned unchanged. +func redactedSource(source string) string { + if strings.HasPrefix(source, "base64://") { + return "base64://[redacted]" + } + return source +} + +// NewFetcher creates a new fetcher instance. +func NewFetcher(opts ...Modifier) *Fetcher { + o := newOpts() + for _, f := range opts { + f(o) + } + return &Fetcher{hc: o.hc, limit: o.limit, cache: o.cache, ttl: o.ttl, schemes: o.schemes} +} + +// FetchContext fetches the file contents from the source and allows to pass a +// context that is used for HTTP requests. +func (f *Fetcher) FetchContext(ctx context.Context, source string) (*bytes.Buffer, error) { + b, err := f.FetchBytes(ctx, source) + if err != nil { + return nil, err + } + return bytes.NewBuffer(b), nil +} + +// FetchBytes fetches the file contents from the source and allows to pass a +// context that is used for HTTP requests. +func (f *Fetcher) FetchBytes(ctx context.Context, source string) ([]byte, error) { + if !slices.ContainsFunc(f.schemes, func(scheme string) bool { + return strings.HasPrefix(source, scheme+"://") + }) { + return nil, errors.WithStack(fmt.Errorf("%w: in source %q: allowed schemes: %s", ErrUnknownScheme, redactedSource(source), strings.Join(f.schemes, ", "))) + } + switch { + case strings.HasPrefix(source, "http://"), strings.HasPrefix(source, "https://"): + return f.fetchRemote(ctx, source) + case strings.HasPrefix(source, "file://"): + b, err := os.ReadFile(strings.TrimPrefix(source, "file://")) + if err != nil { + return nil, errors.Wrapf(err, "read file: %s", redactedSource(source)) + } + return b, nil + case strings.HasPrefix(source, "base64://"): + src, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(source, "base64://")) + if err != nil { + return nil, errors.Wrapf(err, "base64decode: %s", redactedSource(source)) + } + return src, nil + default: + return nil, errors.Wrap(ErrUnknownScheme, "unknown scheme in source: "+redactedSource(source)) + } +} + +func (f *Fetcher) fetchRemote(ctx context.Context, source string) (b []byte, err error) { + if f.cache != nil { + cacheKey := sha256.Sum256([]byte(source)) + if v, ok := f.cache.Get(cacheKey[:]); ok { + b = make([]byte, len(v)) + copy(b, v) + return b, nil + } + defer func() { + if err == nil && len(b) > 0 { + toCache := make([]byte, len(b)) + copy(toCache, b) + f.cache.SetWithTTL(cacheKey[:], toCache, int64(len(toCache)), f.ttl) + } + }() + } + + req, err := retryablehttp.NewRequestWithContext(ctx, http.MethodGet, source, nil) + if err != nil { + return nil, errors.Wrapf(err, "new request: %s", redactedSource(source)) + } + res, err := f.hc.Do(req) + if err != nil { + return nil, errors.Wrap(err, redactedSource(source)) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("expected http response status code 200 but got %d when fetching: %s", res.StatusCode, redactedSource(source)) + } + + if f.limit > 0 { + var buf bytes.Buffer + n, err := io.Copy(&buf, io.LimitReader(res.Body, f.limit+1)) + if n > f.limit { + return nil, bytes.ErrTooLarge + } + if err != nil { + return nil, err + } + return buf.Bytes(), nil + } + return io.ReadAll(res.Body) +} diff --git a/oryx/flagx/flagx.go b/oryx/flagx/flagx.go new file mode 100644 index 0000000..67fb859 --- /dev/null +++ b/oryx/flagx/flagx.go @@ -0,0 +1,108 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package flagx + +import ( + "time" + + "github.com/spf13/pflag" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewFlagSet(name string) *pflag.FlagSet { + return pflag.NewFlagSet(name, pflag.ContinueOnError) +} + +// MustGetBool returns a bool flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetBool(cmd *cobra.Command, name string) bool { + ok, err := cmd.Flags().GetBool(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ok +} + +// MustGetString returns a string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetString(cmd *cobra.Command, name string) string { + s, err := cmd.Flags().GetString(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return s +} + +// MustGetDuration returns a time.Duration flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetDuration(cmd *cobra.Command, name string) time.Duration { + d, err := cmd.Flags().GetDuration(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return d +} + +// MustGetStringSlice returns a []string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringSlice(cmd *cobra.Command, name string) []string { + ss, err := cmd.Flags().GetStringSlice(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetStringArray returns a []string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringArray(cmd *cobra.Command, name string) []string { + ss, err := cmd.Flags().GetStringArray(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetStringToStringMap returns a map[string]string flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetStringToStringMap(cmd *cobra.Command, name string) map[string]string { + ss, err := cmd.Flags().GetStringToString(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetInt returns a int flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetInt(cmd *cobra.Command, name string) int { + ss, err := cmd.Flags().GetInt(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return ss +} + +// MustGetUint8 returns a uint8 flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetUint8(cmd *cobra.Command, name string) uint8 { + v, err := cmd.Flags().GetUint8(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return v +} + +// MustGetUint32 returns a uint32 flag or fatals if an error occurs. +// Deprecated: just handle the error properly, this breaks command testing +func MustGetUint32(cmd *cobra.Command, name string) uint32 { + v, err := cmd.Flags().GetUint32(name) + if err != nil { + cmdx.Fatalf(err.Error()) + } + return v +} diff --git a/oryx/fsx/dirhash.go b/oryx/fsx/dirhash.go new file mode 100644 index 0000000..682f346 --- /dev/null +++ b/oryx/fsx/dirhash.go @@ -0,0 +1,34 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fsx + +import ( + "crypto/sha512" + "io" + "io/fs" +) + +// DirHash computes a directory hash from all files contained in any subdirectories. +func DirHash(dir fs.FS) ([]byte, error) { + hash := sha512.New() + err := fs.WalkDir(dir, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + _, _ = io.WriteString(hash, path) // hash write never errors + f, err := dir.Open(path) + if err != nil { + return err + } + _, _ = io.Copy(hash, f) // hash write never errors + if err = f.Close(); err != nil { + return err + } + return nil + }) + return hash.Sum(nil), err +} diff --git a/oryx/fsx/merge.go b/oryx/fsx/merge.go new file mode 100644 index 0000000..5b758e8 --- /dev/null +++ b/oryx/fsx/merge.go @@ -0,0 +1,229 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package fsx + +import ( + "io" + "io/fs" + "sort" + "time" + + "github.com/pkg/errors" +) + +type ( + mergedFS []fs.FS + mergedFile struct { + files []fs.File + unprocessedDirEntries dirEntries + } + mergedFileInfo []fs.FileInfo + dirEntries []fs.DirEntry +) + +var ( + _ fs.StatFS = (mergedFS)(nil) + _ fs.ReadDirFS = (mergedFS)(nil) + _ fs.ReadDirFile = (*mergedFile)(nil) + _ fs.FileInfo = (mergedFileInfo)(nil) + _ sort.Interface = (dirEntries)(nil) +) + +// Merge multiple filesystems. Later file systems are shadowed by previous ones. +func Merge(fss ...fs.FS) fs.FS { + return mergedFS(fss) +} + +func (m mergedFS) Open(name string) (fs.File, error) { + var file mergedFile + for _, fsys := range m { + f, err := fsys.Open(name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, errors.WithStack(err) + } + + file.files = append(file.files, f) + } + if len(file.files) == 0 { + return nil, errors.WithStack(fs.ErrNotExist) + } + + return &file, nil +} + +func (m mergedFS) Stat(name string) (fs.FileInfo, error) { + for i, fsys := range m { + info, err := fs.Stat(fsys, name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + + switch { + case err != nil: + return nil, errors.WithStack(err) + case info.IsDir(): + dirs := mergedFileInfo{info} + for j := i + 1; j < len(m); j++ { + info, err := fs.Stat(m[j], name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + dirs = append(dirs, info) + } + return dirs, nil + default: + return info, nil + } + } + return nil, errors.WithStack(fs.ErrNotExist) +} + +func (m mergedFS) ReadDir(name string) ([]fs.DirEntry, error) { + var entries dirEntries + + for _, fsys := range m { + e, err := fs.ReadDir(fsys, name) + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return nil, err + } + entries = append(entries, e...) + } + if len(entries) == 0 { + return nil, errors.WithStack(fs.ErrNotExist) + } + + entries.clean() + return entries, nil +} + +func (m mergedFileInfo) Name() string { + return m[0].Name() +} + +func (m mergedFileInfo) Size() int64 { + return m[0].Size() +} + +func (m mergedFileInfo) Mode() fs.FileMode { + return m[0].Mode() +} + +func (m mergedFileInfo) ModTime() time.Time { + return m[0].ModTime() +} + +func (m mergedFileInfo) IsDir() bool { + return m[0].IsDir() +} + +func (m mergedFileInfo) Sys() interface{} { + return m +} + +func (d dirEntries) Len() int { + return len(d) +} + +func (d dirEntries) Less(i, j int) bool { + return d[i].Name() < d[j].Name() +} + +func (d dirEntries) Swap(i, j int) { + d[i], d[j] = d[j], d[i] +} + +func (d *dirEntries) clean() { + sort.Sort(d) + + for i := 1; i < len(*d); i++ { + if (*d)[i-1].Name() == (*d)[i].Name() { + if len(*d)-i == 1 { + // remove the last entry; we're done + *d = (*d)[:i] + return + } + // remove the duplicate entry at index i + *d = append((*d)[:i], (*d)[i+1:]...) + + // need to check the same index again + i-- + } + } +} + +func (m *mergedFile) Stat() (fs.FileInfo, error) { + return m.files[0].Stat() +} + +func (m *mergedFile) Read(bytes []byte) (int, error) { + return m.files[0].Read(bytes) +} + +func (m *mergedFile) Close() error { + var firstErr error + for _, f := range m.files { + if err := f.Close(); err != nil { + if firstErr == nil { + firstErr = errors.WithStack(err) + } + } + } + return firstErr +} + +func (m *mergedFile) ReadDir(n int) ([]fs.DirEntry, error) { + if m.unprocessedDirEntries != nil { + if n <= 0 { + entries := m.unprocessedDirEntries + m.unprocessedDirEntries = nil + return entries, nil + } + if n >= len(m.unprocessedDirEntries) { + entries := m.unprocessedDirEntries + m.unprocessedDirEntries = nil + return entries, io.EOF + } + + var entries dirEntries + entries, m.unprocessedDirEntries = m.unprocessedDirEntries[:n], m.unprocessedDirEntries[n:] + return entries, nil + } + + var entries dirEntries + for _, f := range m.files { + if f, ok := f.(fs.ReadDirFile); ok { + e, err := f.ReadDir(-1) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return nil, err + } + entries = append(entries, e...) + } + } + if entries == nil { + if n > 0 { + return nil, io.EOF + } + return nil, nil + } + + entries.clean() + if n <= 0 { + return entries, nil + } + if n >= len(entries) { + return entries, io.EOF + } + + entries, m.unprocessedDirEntries = entries[:n], entries[n:] + return entries, nil +} diff --git a/oryx/go.mod b/oryx/go.mod new file mode 100644 index 0000000..5559107 --- /dev/null +++ b/oryx/go.mod @@ -0,0 +1,216 @@ +module github.com/ory/x + +go 1.26 + +require ( + code.dny.dev/ssrf v0.2.0 + connectrpc.com/connect v1.20.0 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/auth0/go-jwt-middleware/v2 v2.3.0 + github.com/avast/retry-go/v4 v4.6.1 + github.com/bmatcuk/doublestar/v2 v2.0.4 + github.com/bradleyjkemp/cupaloy/v2 v2.8.0 + github.com/cockroachdb/cockroach-go/v2 v2.4.3 + github.com/dgraph-io/ristretto/v2 v2.4.0 + github.com/evanphx/json-patch/v5 v5.9.11 + github.com/fsnotify/fsnotify v1.9.0 + github.com/ghodss/yaml v1.0.0 + github.com/go-jose/go-jose/v3 v3.0.5 + github.com/go-openapi/jsonpointer v0.21.2 + github.com/go-openapi/runtime v0.28.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/gobuffalo/httptest v1.5.2 + github.com/gobwas/glob v0.2.3 + github.com/goccy/go-yaml v1.18.0 + github.com/gofrs/flock v0.12.1 + github.com/gofrs/uuid v4.4.0+incompatible + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/go-jsonnet v0.21.0 + github.com/gorilla/securecookie v1.1.2 + github.com/gorilla/websocket v1.5.3 + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 + github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf + github.com/jackc/pgx/v5 v5.10.0 + github.com/jackc/puddle/v2 v2.2.2 + github.com/jmoiron/sqlx v1.4.0 + github.com/knadh/koanf/maps v0.1.2 + github.com/knadh/koanf/parsers/json v0.1.0 + github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/parsers/yaml v0.1.0 + github.com/knadh/koanf/providers/posflag v0.1.0 + github.com/knadh/koanf/providers/rawbytes v0.1.0 + github.com/knadh/koanf/v2 v2.2.2 + github.com/laher/mergefs v0.1.1 + github.com/landlock-lsm/go-landlock v0.8.1 + github.com/lestrrat-go/jwx v1.2.31 + github.com/mailhog/data v1.0.1 + github.com/moby/moby/client v0.4.1 + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 + github.com/ory/analytics-go/v5 v5.0.1 + github.com/ory/dockertest/v4 v4.0.0-beta.4 + github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 + github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e + github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 + github.com/pelletier/go-toml v1.9.5 + github.com/peterhellberg/link v1.2.0 + github.com/pkg/errors v0.9.1 + github.com/pkg/profile v1.7.0 + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 + github.com/prometheus/common v0.68.1 + github.com/rakutentech/jwk-go v1.2.0 + github.com/rs/cors v1.11.1 + github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cast v1.9.2 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 + github.com/tidwall/gjson v1.19.0 + github.com/tidwall/sjson v1.2.5 + github.com/urfave/negroni v1.0.0 + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 + go.opentelemetry.io/contrib/propagators/b3 v1.44.0 + go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 + go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 + go.opentelemetry.io/otel v1.44.0 + go.opentelemetry.io/otel/exporters/jaeger v1.17.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 + go.opentelemetry.io/otel/exporters/zipkin v1.44.0 + go.opentelemetry.io/otel/sdk v1.44.0 + go.opentelemetry.io/otel/trace v1.44.0 + go.opentelemetry.io/proto/otlp v1.10.0 + go.uber.org/goleak v1.3.0 + go.uber.org/mock v0.5.2 + golang.org/x/crypto v0.53.0 + golang.org/x/net v0.56.0 + golang.org/x/oauth2 v0.36.0 + golang.org/x/sync v0.21.0 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 + modernc.org/sqlite v1.52.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + filippo.io/edwards25519 v1.2.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/XSAM/otelsql v0.41.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aymerick/douceur v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/errors v0.22.2 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/gobuffalo/envy v1.10.2 // indirect + github.com/gobuffalo/fizz v1.14.4 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect + github.com/gobuffalo/github_flavored_markdown v1.1.4 // indirect + github.com/gobuffalo/helpers v0.6.10 // indirect + github.com/gobuffalo/nulls v0.4.2 // indirect + github.com/gobuffalo/plush/v4 v4.1.22 // indirect + github.com/gobuffalo/plush/v5 v5.0.7 // indirect + github.com/gobuffalo/tags/v3 v3.1.4 // indirect + github.com/gobuffalo/validate/v3 v3.3.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/gogo/googleapis v1.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/css v1.0.1 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jaegertracing/jaeger-idl v0.9.0 // indirect + github.com/jandelgado/gcov2lcov v1.1.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/iter v1.0.2 // indirect + github.com/lestrrat-go/option v1.0.1 // indirect + github.com/lib/pq v1.12.0 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect + github.com/microcosm-cc/bluemonday v1.0.27 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nyaruka/phonenumbers v1.6.5 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/segmentio/backo-go v1.1.0 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect + github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect + go.mongodb.org/mongo-driver v1.17.7 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/metric v1.44.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 // indirect + modernc.org/libc v1.72.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +tool ( + github.com/jandelgado/gcov2lcov + go.uber.org/mock/mockgen + golang.org/x/tools/cmd/goimports +) diff --git a/oryx/go.sum b/oryx/go.sum new file mode 100644 index 0000000..30bce92 --- /dev/null +++ b/oryx/go.sum @@ -0,0 +1,636 @@ +code.dny.dev/ssrf v0.2.0 h1:wCBP990rQQ1CYfRpW+YK1+8xhwUjv189AQ3WMo1jQaI= +code.dny.dev/ssrf v0.2.0/go.mod h1:B+91l25OnyaLIeCx0WRJN5qfJ/4/ZTZxRXgm0lj/2w8= +connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= +connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8= +github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE= +github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= +github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cockroachdb/cockroach-go/v2 v2.4.3 h1:LJO3K3jC5WXvMePRQSJE1NsIGoFGcEx1LW83W6RAlhw= +github.com/cockroachdb/cockroach-go/v2 v2.4.3/go.mod h1:9U179XbCx4qFWtNhc7BiWLPfuyMVQ7qdAhfrwLz1vH0= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= +github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= +github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= +github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= +github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= +github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg= +github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0= +github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA= +github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco= +github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs= +github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ= +github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc= +github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY= +github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= +github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= +github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= +github.com/gobuffalo/fizz v1.14.4 h1:8uume7joF6niTNWN582IQ2jhGTUoa9g1fiV/tIoGdBs= +github.com/gobuffalo/fizz v1.14.4/go.mod h1:9/2fGNXNeIFOXEEgTPJwiK63e44RjG+Nc4hfMm1ArGM= +github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/github_flavored_markdown v1.1.3/go.mod h1:IzgO5xS6hqkDmUh91BW/+Qxo/qYnvfzoz3A7uLkg77I= +github.com/gobuffalo/github_flavored_markdown v1.1.4 h1:WacrEGPXUDX+BpU1GM/Y0ADgMzESKNWls9hOTG1MHVs= +github.com/gobuffalo/github_flavored_markdown v1.1.4/go.mod h1:Vl9686qrVVQou4GrHRK/KOG3jCZOKLUqV8MMOAYtlso= +github.com/gobuffalo/helpers v0.6.7/go.mod h1:j0u1iC1VqlCaJEEVkZN8Ia3TEzfj/zoXANqyJExTMTA= +github.com/gobuffalo/helpers v0.6.10 h1:puKDCOrJ0EIq5ScnTRgKyvEZ05xQa+gwRGCpgoh6Ek8= +github.com/gobuffalo/helpers v0.6.10/go.mod h1:r52L6VSnByLJFOmURp1irvzgSakk7RodChi1YbGwk8I= +github.com/gobuffalo/httptest v1.5.2 h1:GpGy520SfY1QEmyPvaqmznTpG4gEQqQ82HtHqyNEreM= +github.com/gobuffalo/httptest v1.5.2/go.mod h1:FA23yjsWLGj92mVV74Qtc8eqluc11VqcWr8/C1vxt4g= +github.com/gobuffalo/nulls v0.4.2 h1:GAqBR29R3oPY+WCC7JL9KKk9erchaNuV6unsOSZGQkw= +github.com/gobuffalo/nulls v0.4.2/go.mod h1:EElw2zmBYafU2R9W4Ii1ByIj177wA/pc0JdjtD0EsH8= +github.com/gobuffalo/plush/v4 v4.1.16/go.mod h1:6t7swVsarJ8qSLw1qyAH/KbrcSTwdun2ASEQkOznakg= +github.com/gobuffalo/plush/v4 v4.1.22 h1:bPQr5PsiTg54UGMsfvnIAvFmUfxzD/ri+wbpu7PlmTM= +github.com/gobuffalo/plush/v4 v4.1.22/go.mod h1:WiKHJx3qBvfaDVlrv8zT7NCd3dEMaVR/fVxW4wqV17M= +github.com/gobuffalo/plush/v5 v5.0.7 h1:nI8sIt5tZAN2tCZHeaXkH7HAvxvvk3sJHG2TtrKeSHM= +github.com/gobuffalo/plush/v5 v5.0.7/go.mod h1:C08u/VEqzzPBXFF/yqs40P/5Cvc/zlZsMzhCxXyWJmU= +github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= +github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= +github.com/gobuffalo/validate/v3 v3.3.3 h1:o7wkIGSvZBYBd6ChQoLxkz2y1pfmhbI4jNJYh6PuNJ4= +github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= +github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-jsonnet v0.21.0 h1:43Bk3K4zMRP/aAZm9Po2uSEjY6ALCkYUVIcz9HLGMvA= +github.com/google/go-jsonnet v0.21.0/go.mod h1:tCGAu8cpUpEZcdGMmdOu37nh8bGgqubhI5v2iSk3KJQ= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= +github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= +github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jaegertracing/jaeger-idl v0.9.0 h1:dI4olA7ArW3cjXwVbic/aYKDbdlfe7V+9wPQqAdzu8Y= +github.com/jaegertracing/jaeger-idl v0.9.0/go.mod h1:W+9vbcr2cVZyS6z/cbr540EOzSkKYml3hmaWEavxkB0= +github.com/jandelgado/gcov2lcov v1.1.1 h1:CHUNoAglvb34DqmMoZchnzDbA3yjpzT8EoUvVqcAY+s= +github.com/jandelgado/gcov2lcov v1.1.1/go.mod h1:tMVUlMVtS1po2SB8UkADWhOT5Y5Q13XOce2AYU69JuI= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= +github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= +github.com/knadh/koanf/parsers/json v0.1.0/go.mod h1:ll2/MlXcZ2BfXD6YJcjVFzhG9P0TdJ207aIBKQhV2hY= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= +github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= +github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= +github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/laher/mergefs v0.1.1 h1:nV2bTS57vrmbMxeR6uvJpI8LyGl3QHj4bLBZO3aUV58= +github.com/laher/mergefs v0.1.1/go.mod h1:FSY1hYy94on4Tz60waRMGdO1awwS23BacqJlqf9lJ9Q= +github.com/landlock-lsm/go-landlock v0.8.1 h1:Krs1co16IzN7bQcFYIdtNF+BKwZem3geRBkVsZtlCKU= +github.com/landlock-lsm/go-landlock v0.8.1/go.mod h1:mn5GSi81Jf7yMs5WSi+SUi4sUeNLUGVdbT4Id6wXNQw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= +github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= +github.com/lestrrat-go/jwx v1.2.31 h1:/OM9oNl/fzyldpv5HKZ9m7bTywa7COUfg8gujd9nJ54= +github.com/lestrrat-go/jwx v1.2.31/go.mod h1:eQJKoRwWcLg4PfD5CFA5gIZGxhPgoPYq9pZISdxLf0c= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= +github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mailhog/data v1.0.1 h1:7I+opBvVdi4EMJaihXavM98jp/ovt4o6mz47446RAW8= +github.com/mailhog/data v1.0.1/go.mod h1:tjR/iXRhbSUKHzAAMd99RygVaDB5rIDC/bmWc363MzU= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/microcosm-cc/bluemonday v1.0.20/go.mod h1:yfBmMi8mxvaZut3Yytv+jTXRY8mxyjJ0/kQBTElld50= +github.com/microcosm-cc/bluemonday v1.0.22/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= +github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= +github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nyaruka/phonenumbers v1.6.5 h1:aBCaUhfpRA7hU6fsXk+p7KF1aNx4nQlq9hGeo2qdFg8= +github.com/nyaruka/phonenumbers v1.6.5/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/ory/analytics-go/v5 v5.0.1 h1:LX8T5B9FN8KZXOtxgN+R3I4THRRVB6+28IKgKBpXmAM= +github.com/ory/analytics-go/v5 v5.0.1/go.mod h1:lWCiCjAaJkKfgR/BN5DCLMol8BjKS1x+4jxBxff/FF0= +github.com/ory/dockertest/v4 v4.0.0-beta.4 h1:QcrNrobOP+5IjSDmS4//EuBtwiFuznQhi5xTe8oFSoM= +github.com/ory/dockertest/v4 v4.0.0-beta.4/go.mod h1:p9kfE14tzK8+WU4F9YbIZlzhCzQ2pH7H1KIfBKrF3DM= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22 h1:xSbAB0yH2CUPqf9tiei3EFo7xpqY5nsAGdcrwqH9o6o= +github.com/ory/herodot v0.10.9-0.20260330111132-da75ef0fbc22/go.mod h1:by6EjQDF4bDC2OuWOBd9EBRrTjtl0+kxSzyvuizM0Iw= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e h1:4tUrC7x4YWRVMFp+c64KACNSGchW1zXo4l6Pa9/1hA8= +github.com/ory/jsonschema/v3 v3.0.9-0.20250317235931-280c5fc7bf0e/go.mod h1:XWLxVK4un/iuIcrw+6lCeanbF3NZwO5k6RdLeu/loQk= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6 h1:LNreY5V2pXsUgiY/rVaU5uhIsOUHzZ+PP/NUgfqubr0= +github.com/ory/pop/v6 v6.4.2-0.20260714091811-7f2223c43ab6/go.mod h1:vd8H2inBRK+ZF+r5jDdx/fYPwqWrbVoAD4t7vAfXeA4= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterhellberg/link v1.2.0 h1:UA5pg3Gp/E0F2WdX7GERiNrPQrM1K6CVJUUWfHa4t6c= +github.com/peterhellberg/link v1.2.0/go.mod h1:gYfAh+oJgQu2SrZHg5hROVRQe1ICoK0/HHJTcE0edxc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= +github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rakutentech/jwk-go v1.2.0 h1:vNJwedPkRR+32V5WGNj0JP4COes93BGERvzQLBjLy4c= +github.com/rakutentech/jwk-go v1.2.0/go.mod h1:pI0bYVntqaJ27RCpaC75MTUacheW0Rk4+8XzWWe1OWM= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761 h1:0b8DF5kR0PhRoRXDiEEdzrgBc8UqVY4JWLkQJCRsLME= +github.com/seatgeek/logrus-gelf-formatter v0.0.0-20210414080842-5b05eb8ff761/go.mod h1:/THDZYi7F/BsVEcYzYPqdcWFQ+1C2InkawTKfLOAnzg= +github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= +github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= +github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= +github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= +github.com/segmentio/conf v1.2.0/go.mod h1:Y3B9O/PqqWqjyxyWWseyj/quPEtMu1zDp/kVbSWWaB0= +github.com/segmentio/go-snakecase v1.1.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= +github.com/segmentio/objconv v1.0.1/go.mod h1:auayaH5k3137Cl4SoXTgrzQcuQDmvuVtZgS0fb1Ahys= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= +github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= +github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= +github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU= +go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0 h1:MCcYL7J6Vt/X0kjqbMZkekCmwsurbQRbL69vkiye2lk= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.69.0/go.mod h1:3jnStNwSufK+f5ktjL4EPcwtig4rtd81NS70lqHuXl8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0 h1:1IFH4oFKK8KupzIelCl3u+bkxpGRps1oWRjQI2+TTWs= +go.opentelemetry.io/contrib/propagators/b3 v1.44.0/go.mod h1:JqWFXsc7VDaqIyubFhEd2cPHqsrzqP0Lvn783SUwyro= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0 h1:OyzvsAMc/zHt0DRPcfstn0wgfq8ApDkeY0ABMcueweM= +go.opentelemetry.io/contrib/propagators/jaeger v1.44.0/go.mod h1:44kghcGX+BNxy9UTiWtd6VDt8Nd4EypGBkH2+v2Dqrc= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1 h1:pV2nZ1iE87X9ym+crkD9k15zTLbN08+IC4C7sk9sQYM= +go.opentelemetry.io/contrib/samplers/jaegerremote v0.37.1/go.mod h1:nvgrM8LaG2+5G7WxbtjEPiSkg87+d0/ltZZK70p7FVo= +go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= +go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4= +go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0 h1:zv7PRYGLrQHkdeZj0c5SNAZOJcw55XgaTezUkNpwA+w= +go.opentelemetry.io/otel/exporters/zipkin v1.44.0/go.mod h1:3+VZyCi6hFW+UuxFF+wSOvwsOwncfBpQfP7Qdb3JXKg= +go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= +go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= +go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= +go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= +go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +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-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8= +google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= +gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/mold.v2 v2.2.0/go.mod h1:XMyyRsGtakkDPbxXbrA5VODo6bUXyvoDjLd5l3T0XoA= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77 h1:Z06sMOzc0GNCwp6efaVrIrz4ywGJ1v+DP0pjVkOfDuA= +kernel.org/pub/linux/libs/security/libcap/psx v1.2.77/go.mod h1:+l6Ee2F59XiJ2I6WR5ObpC1utCQJZ/VLsEbQCD8RG24= +modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY= +modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ= +modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= +modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU= +modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.52.0 h1:p4dhYh2tXZCiyaqHwRVJDjIGKWyXayiQpThxgDzJaxo= +modernc.org/sqlite v1.52.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/oryx/hasherx/hash_comparator.go b/oryx/hasherx/hash_comparator.go new file mode 100644 index 0000000..be2745f --- /dev/null +++ b/oryx/hasherx/hash_comparator.go @@ -0,0 +1,227 @@ +package hasherx + +import ( + "context" + "crypto/subtle" + "encoding/base64" + "fmt" + "math" + "regexp" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/pbkdf2" +) + +var ErrUnknownHashAlgorithm = errors.New("unknown hash algorithm") + +// Compare the given password with the given hash. +func Compare(ctx context.Context, password []byte, hash []byte) error { + switch { + case IsBcryptHash(hash): + return CompareBcrypt(ctx, password, hash) + case IsArgon2idHash(hash): + return CompareArgon2id(ctx, password, hash) + case IsArgon2iHash(hash): + return CompareArgon2i(ctx, password, hash) + case IsPbkdf2Hash(hash): + return ComparePbkdf2(ctx, password, hash) + default: + return errors.WithStack(ErrUnknownHashAlgorithm) + } +} + +func CompareBcrypt(_ context.Context, password []byte, hash []byte) error { + if err := validateBcryptPasswordLength(password); err != nil { + return err + } + + err := bcrypt.CompareHashAndPassword(hash, password) + if err != nil { + return err + } + + return nil +} + +func CompareArgon2id(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + mem := uint64(p.Memory) + if mem > math.MaxUint32 { + return errors.WithStack(ErrInvalidHash) + } + + // Derive the key from the other password using the same parameters. + otherHash := argon2.IDKey(password, salt, p.Iterations, uint32(mem), p.Parallelism, p.KeyLength) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +func CompareArgon2i(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodeArgon2idHash(string(hash)) + if err != nil { + return err + } + + mem := uint64(p.Memory) + if mem > math.MaxUint32 { + return errors.WithStack(ErrInvalidHash) + } + + // Derive the key from the other password using the same parameters. + otherHash := argon2.Key(password, salt, p.Iterations, uint32(mem), p.Parallelism, p.KeyLength) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +func ComparePbkdf2(_ context.Context, password []byte, hash []byte) error { + // Extract the parameters, salt and derived key from the encoded password + // hash. + p, salt, hash, err := decodePbkdf2Hash(string(hash)) + if err != nil { + return err + } + + // Derive the key from the other password using the same parameters. + otherHash := pbkdf2.Key(password, salt, int(p.Iterations), int(p.KeyLength), getPseudorandomFunctionForPbkdf2(p.Algorithm)) + + // Check that the contents of the hashed passwords are identical. Note + // that we are using the subtle.ConstantTimeCompare() function for this + // to help prevent timing attacks. + if subtle.ConstantTimeCompare(hash, otherHash) == 1 { + return nil + } + return errors.WithStack(ErrMismatchedHashAndPassword) +} + +var ( + isBcryptHash = regexp.MustCompile(`^\$2[abzy]?\$`) + isArgon2idHash = regexp.MustCompile(`^\$argon2id\$`) + isArgon2iHash = regexp.MustCompile(`^\$argon2i\$`) + isPbkdf2Hash = regexp.MustCompile(`^\$pbkdf2-sha[0-9]{1,3}\$`) +) + +func IsBcryptHash(hash []byte) bool { + return isBcryptHash.Match(hash) +} + +func IsArgon2idHash(hash []byte) bool { + return isArgon2idHash.Match(hash) +} + +func IsArgon2iHash(hash []byte) bool { + return isArgon2iHash.Match(hash) +} + +func IsPbkdf2Hash(hash []byte) bool { + return isPbkdf2Hash.Match(hash) +} + +func decodeArgon2idHash(encodedHash string) (p *Argon2Config, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 6 { + return nil, nil, nil, ErrInvalidHash + } + + var version int + _, err = fmt.Sscanf(parts[2], "v=%d", &version) + if err != nil { + return nil, nil, nil, err + } + if version != argon2.Version { + return nil, nil, nil, ErrIncompatibleVersion + } + + p = new(Argon2Config) + _, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.Memory, &p.Iterations, &p.Parallelism) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + saltLength := uint(len(salt)) + if saltLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.SaltLength = uint32(saltLength) + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[5]) + if err != nil { + return nil, nil, nil, err + } + keyLength := uint(len(hash)) + if keyLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.KeyLength = uint32(keyLength) + + return p, salt, hash, nil +} + +// decodePbkdf2Hash decodes PBKDF2 encoded password hash. +// format: $pbkdf2-$i=,l=$$ +func decodePbkdf2Hash(encodedHash string) (p *PBKDF2Config, salt, hash []byte, err error) { + parts := strings.Split(encodedHash, "$") + if len(parts) != 5 { + return nil, nil, nil, ErrInvalidHash + } + + p = new(PBKDF2Config) + digestParts := strings.SplitN(parts[1], "-", 2) + if len(digestParts) != 2 { + return nil, nil, nil, ErrInvalidHash + } + p.Algorithm = digestParts[1] + + _, err = fmt.Sscanf(parts[2], "i=%d,l=%d", &p.Iterations, &p.KeyLength) + if err != nil { + return nil, nil, nil, err + } + + salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[3]) + if err != nil { + return nil, nil, nil, err + } + saltLength := uint(len(salt)) + if saltLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.SaltLength = uint32(saltLength) + + hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) + if err != nil { + return nil, nil, nil, err + } + keyLength := uint(len(hash)) + if keyLength > math.MaxUint32 { + return nil, nil, nil, ErrInvalidHash + } + p.KeyLength = uint32(keyLength) + + return p, salt, hash, nil +} diff --git a/oryx/hasherx/hasher.go b/oryx/hasherx/hasher.go new file mode 100644 index 0000000..c254724 --- /dev/null +++ b/oryx/hasherx/hasher.go @@ -0,0 +1,20 @@ +package hasherx + +import ( + "context" +) + +// Hasher provides methods for generating and comparing password hashes. +type Hasher interface { + // Generate returns a hash derived from the password or an error if the hash method failed. + Generate(ctx context.Context, password []byte) ([]byte, error) + + // Understands returns whether the given hash can be understood by this hasher. + Understands(hash []byte) bool +} + +type HashProvider interface { + Hasher() Hasher +} + +const tracingComponent = "github.com/ory/kratos/hash" diff --git a/oryx/hasherx/hasher_argon2.go b/oryx/hasherx/hasher_argon2.go new file mode 100644 index 0000000..238ab39 --- /dev/null +++ b/oryx/hasherx/hasher_argon2.go @@ -0,0 +1,118 @@ +package hasherx + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "math" + "time" + + "github.com/ory/x/otelx" + + "github.com/inhies/go-bytesize" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + + "github.com/pkg/errors" + "golang.org/x/crypto/argon2" +) + +var ( + ErrInvalidHash = errors.New("the encoded hash is not in the correct format") + ErrIncompatibleVersion = errors.New("incompatible version of argon2") + ErrMismatchedHashAndPassword = errors.New("passwords do not match") +) + +type ( + // Argon2Config is the configuration for a Argon2 hasher. + Argon2Config struct { + // Memory is the amount of memory to use. + Memory bytesize.ByteSize `json:"memory"` + + // Iterations is the number of iterations to use. + Iterations uint32 `json:"iterations"` + + // Parallelism is the number of threads to use. + Parallelism uint8 `json:"parallelism"` + + // SaltLength is the length of the salt to use. + SaltLength uint32 `json:"salt_length"` + + // KeyLength is the length of the key to use. + KeyLength uint32 `json:"key_length"` + + // ExpectedDuration is the expected duration of the hash. + ExpectedDuration time.Duration `json:"expected_duration"` + + // ExpectedDeviation is the expected deviation of the hash. + ExpectedDeviation time.Duration `json:"expected_deviation"` + + // DedicatedMemory is the amount of dedicated memory to use. + DedicatedMemory bytesize.ByteSize `json:"dedicated_memory"` + } + // Argon2 is a hasher that uses the Argon2 algorithm. + Argon2 struct { + c Argon2Configurator + } + // Argon2Configurator is a function that returns the Argon2 configuration. + Argon2Configurator interface { + HasherArgon2Config(ctx context.Context) *Argon2Config + } +) + +func NewHasherArgon2(c Argon2Configurator) *Argon2 { + return &Argon2{c: c} +} + +func toKB(mem bytesize.ByteSize) (uint32, error) { + kb := uint64(mem / bytesize.KB) + if kb > math.MaxUint32 { + return 0, errors.Errorf("memory %v is too large", mem) + } + return uint32(kb), nil +} + +// Generate generates a hash for the given password. +func (h *Argon2) Generate(ctx context.Context, password []byte) (_ []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Argon2.Generate") + defer otelx.End(span, &err) + p := h.c.HasherArgon2Config(ctx) + span.SetAttributes(attribute.String("argon2.config", fmt.Sprintf("#%v", p))) + + salt := make([]byte, p.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + mem, err := toKB(p.Memory) + if err != nil { + return nil, err + } + // Pass the plaintext password, salt and parameters to the argon2.IDKey + // function. This will generate a hash of the password using the Argon2id + // variant. + hash := argon2.IDKey(password, salt, p.Iterations, mem, p.Parallelism, p.KeyLength) + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", + argon2.Version, mem, p.Iterations, p.Parallelism, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(hash), + ); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +// Understands checks if the given hash is in the correct format. +func (h *Argon2) Understands(hash []byte) bool { + return IsArgon2idHash(hash) +} diff --git a/oryx/hasherx/hasher_bcrypt.go b/oryx/hasherx/hasher_bcrypt.go new file mode 100644 index 0000000..64be1f4 --- /dev/null +++ b/oryx/hasherx/hasher_bcrypt.go @@ -0,0 +1,68 @@ +package hasherx + +import ( + "context" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "golang.org/x/crypto/bcrypt" + + "github.com/ory/x/otelx" +) + +// ErrBcryptPasswordLengthReached is returned when the password is longer than 72 bytes. +var ErrBcryptPasswordLengthReached = errors.Errorf("passwords are limited to a maximum length of 72 characters") + +type ( + // Bcrypt is a hasher that uses the bcrypt algorithm. + Bcrypt struct { + c BCryptConfigurator + } + // BCryptConfig is the configuration for the bcrypt hasher. + BCryptConfig struct { + Cost uint32 `json:"cost"` + } + // BCryptConfigurator is the interface that must be implemented by a configuration provider for the bcrypt hasher. + BCryptConfigurator interface { + HasherBcryptConfig(ctx context.Context) *BCryptConfig + } +) + +func NewHasherBcrypt(c BCryptConfigurator) *Bcrypt { + return &Bcrypt{c: c} +} + +// Generate generates a hash for the given password. +func (h *Bcrypt) Generate(ctx context.Context, password []byte) (hash []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer(tracingComponent).Start(ctx, "hash.Bcrypt.Generate") + defer otelx.End(span, &err) + + if err := validateBcryptPasswordLength(password); err != nil { + return nil, err + } + + cost := int(h.c.HasherBcryptConfig(ctx).Cost) + span.SetAttributes(attribute.Int("bcrypt.cost", cost)) + hash, err = bcrypt.GenerateFromPassword(password, cost) + if err != nil { + return nil, err + } + + return hash, nil +} + +func validateBcryptPasswordLength(password []byte) error { + // Bcrypt truncates the password to the first 72 bytes, following the OpenBSD implementation, + // so if password is longer than 72 bytes, function returns an error + // See https://en.wikipedia.org/wiki/Bcrypt#User_input + if len(password) > 72 { + return ErrBcryptPasswordLengthReached + } + return nil +} + +// Understands checks if the given hash is in the correct format. +func (h *Bcrypt) Understands(hash []byte) bool { + return IsBcryptHash(hash) +} diff --git a/oryx/hasherx/hasher_pbkdf2.go b/oryx/hasherx/hasher_pbkdf2.go new file mode 100644 index 0000000..de65955 --- /dev/null +++ b/oryx/hasherx/hasher_pbkdf2.go @@ -0,0 +1,103 @@ +package hasherx + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha1" // #nosec G505 - compatibility for imported passwords + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "fmt" + "hash" + + "github.com/pkg/errors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/codes" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/sha3" + + "github.com/ory/x/otelx" +) + +type ( + // PBKDF2 is a PBKDF2 hasher. + PBKDF2 struct { + c PBKDF2Configurator + } + + // PBKDF2Config is the configuration for a PBKDF2 hasher. + PBKDF2Config struct { + // Algorithm can be one of sha1, sha224, sha256, sha384, sha512 + Algorithm string + // Iterations is the number of iterations to use. + Iterations uint32 + // KeyLength is the length of the salt. + SaltLength uint32 + // KeyLength is the length of the key. + KeyLength uint32 + } + + // PBKDF2Configurator is a configurator for a PBKDF2 hasher. + PBKDF2Configurator interface { + HasherPBKDF2Config(ctx context.Context) *PBKDF2Config + } +) + +// NewHasherPBKDF2 creates a new PBKDF2 hasher. +func NewHasherPBKDF2(c PBKDF2Configurator) *PBKDF2 { + return &PBKDF2{c: c} +} + +// Generate generates a hash for the given password. +func (h *PBKDF2) Generate(ctx context.Context, password []byte) (hash []byte, err error) { + ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "hash.PBKDF2.Generate") + defer otelx.End(span, &err) + + conf := h.c.HasherPBKDF2Config(ctx) + salt := make([]byte, conf.SaltLength) + if _, err := rand.Read(salt); err != nil { + return nil, err + } + + key := pbkdf2.Key(password, salt, int(conf.Iterations), int(conf.KeyLength), getPseudorandomFunctionForPbkdf2(conf.Algorithm)) + + var b bytes.Buffer + if _, err := fmt.Fprintf( + &b, + "$pbkdf2-%s$i=%d,l=%d$%s$%s", + conf.Algorithm, + conf.Iterations, + conf.KeyLength, + base64.RawStdEncoding.EncodeToString(salt), + base64.RawStdEncoding.EncodeToString(key), + ); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, errors.WithStack(err) + } + + return b.Bytes(), nil +} + +// Understands checks if the given hash is in the correct format. +func (h *PBKDF2) Understands(hash []byte) bool { + return IsPbkdf2Hash(hash) +} + +func getPseudorandomFunctionForPbkdf2(alg string) func() hash.Hash { + switch alg { + case "sha1": + return sha1.New + case "sha224": + return sha3.New224 + case "sha256": + return sha256.New + case "sha384": + return sha3.New384 + case "sha512": + return sha512.New + default: + return sha256.New + } +} diff --git a/oryx/healthx/doc.go b/oryx/healthx/doc.go new file mode 100644 index 0000000..200b47d --- /dev/null +++ b/oryx/healthx/doc.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package healthx providers helpers for returning health status information via HTTP. +package healthx + +import "strings" + +// The health status of the service. +// +// swagger:model healthStatus +type swaggerHealthStatus struct { + // Status always contains "ok". + Status string `json:"status"` +} + +// The not ready status of the service. +// +// swagger:model healthNotReadyStatus +type swaggerNotReadyStatus struct { + // Errors contains a list of errors that caused the not ready status. + Errors map[string]string `json:"errors"` +} + +func (s swaggerNotReadyStatus) Error() string { + var errs []string + for _, err := range s.Errors { + errs = append(errs, err) + } + return strings.Join(errs, "; ") +} + +// swagger:model version +type swaggerVersion struct { + // Version is the service's version. + Version string `json:"version"` +} diff --git a/oryx/healthx/handler.go b/oryx/healthx/handler.go new file mode 100644 index 0000000..092464a --- /dev/null +++ b/oryx/healthx/handler.go @@ -0,0 +1,222 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package healthx + +import ( + "net/http" + + "github.com/ory/herodot" + "github.com/ory/x/httprouterx" +) + +const ( + // AliveCheckPath is the path where information about the life state of the instance is provided. + AliveCheckPath = "/health/alive" + // ReadyCheckPath is the path where information about the ready state of the instance is provided. + ReadyCheckPath = "/health/ready" + // VersionPath is the path where information about the software version of the instance is provided. + VersionPath = "/version" +) + +// RoutesToObserve returns a string of all the available routes of this module. +func RoutesToObserve() []string { + return []string{ + AliveCheckPath, + ReadyCheckPath, + VersionPath, + } +} + +// ReadyChecker should return an error if the component is not ready yet. +type ReadyChecker func(r *http.Request) error + +// ReadyCheckers is a map of ReadyCheckers. +type ReadyCheckers map[string]ReadyChecker + +// NoopReadyChecker is always ready. +func NoopReadyChecker() error { + return nil +} + +// Handler handles HTTP requests to health and version endpoints. +type Handler struct { + H herodot.Writer + VersionString string + ReadyChecks ReadyCheckers +} + +type options struct { + middleware func(http.Handler) http.Handler +} + +type Options func(*options) + +// NewHandler instantiates a handler. +func NewHandler( + h herodot.Writer, + version string, + readyChecks ReadyCheckers, +) *Handler { + return &Handler{ + H: h, + VersionString: version, + ReadyChecks: readyChecks, + } +} + +// SetHealthRoutes registers this handler's routes for health checking. +func (h *Handler) SetHealthRoutes(r httprouterx.Router, shareErrors bool, opts ...Options) { + o := &options{} + aliveHandler := h.Alive() + readyHandler := h.Ready(shareErrors) + + for _, opt := range opts { + opt(o) + } + + if o.middleware != nil { + aliveHandler = o.middleware(aliveHandler) + readyHandler = o.middleware(readyHandler) + } + + r.GET(AliveCheckPath, aliveHandler.ServeHTTP) + r.GET(ReadyCheckPath, readyHandler.ServeHTTP) +} + +// SetVersionRoutes registers this handler's routes for health checking. +func (h *Handler) SetVersionRoutes(r httprouterx.Router, opts ...Options) { + o := &options{} + versionHandler := h.Version() + + for _, opt := range opts { + opt(o) + } + + if o.middleware != nil { + versionHandler = o.middleware(versionHandler) + } + + r.GET(VersionPath, versionHandler.ServeHTTP) +} + +// Alive returns an ok status if the instance is ready to handle HTTP requests. +// +// swagger:route GET /health/alive health isInstanceAlive +// +// # Check alive status +// +// This endpoint returns a 200 status code when the HTTP server is up running. +// This status does currently not include checks whether the database connection is working. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// Be aware that if you are running multiple nodes of this service, the health status will never +// refer to the cluster state, only to a single instance. +// +// Produces: +// - application/json +// - text/plain +// +// Responses: +// 200: healthStatus +// default: unexpectedError +func (h *Handler) Alive() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + h.H.Write(rw, r, &swaggerHealthStatus{ + Status: "ok", + }) + }) +} + +// swagger:model unexpectedError +// +//nolint:deadcode,unused +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type unexpectedError string + +// Ready returns an ok status if the instance is ready to handle HTTP requests and all ReadyCheckers are ok. +// +// swagger:route GET /health/ready health isInstanceReady +// +// # Check readiness status +// +// This endpoint returns a 200 status code when the HTTP server is up running and the environment dependencies (e.g. +// the database) are responsive as well. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// Be aware that if you are running multiple nodes of this service, the health status will never +// refer to the cluster state, only to a single instance. +// +// Produces: +// - application/json +// - text/plain +// +// Responses: +// 200: healthStatus +// 503: healthNotReadyStatus +// default: unexpectedError +func (h *Handler) Ready(shareErrors bool) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + var notReady = swaggerNotReadyStatus{ + Errors: map[string]string{}, + } + + for n, c := range h.ReadyChecks { + if err := c(r); err != nil { + if shareErrors { + notReady.Errors[n] = err.Error() + } else { + notReady.Errors[n] = "error may contain sensitive information and was obfuscated" + } + } + } + + if len(notReady.Errors) > 0 { + h.H.WriteErrorCode(rw, r, http.StatusServiceUnavailable, ¬Ready) + return + } + + h.H.Write(rw, r, &swaggerHealthStatus{ + Status: "ok", + }) + }) +} + +// Version returns this service's versions. +// +// swagger:route GET /version version getVersion +// +// # Get service version +// +// This endpoint returns the service version typically notated using semantic versioning. +// +// If the service supports TLS Edge Termination, this endpoint does not require the +// `X-Forwarded-Proto` header to be set. +// +// Be aware that if you are running multiple nodes of this service, the health status will never +// refer to the cluster state, only to a single instance. +// +// Produces: +// - application/json +// +// Responses: +// 200: version +func (h *Handler) Version() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + h.H.Write(rw, r, &swaggerVersion{ + Version: h.VersionString, + }) + }) +} + +// WithMiddleware accepts a http.Handler to be run on the +// route handlers +func WithMiddleware(h func(http.Handler) http.Handler) Options { + return func(o *options) { + o.middleware = h + } +} diff --git a/oryx/healthx/openapi/patch.yaml b/oryx/healthx/openapi/patch.yaml new file mode 100644 index 0000000..2d4baef --- /dev/null +++ b/oryx/healthx/openapi/patch.yaml @@ -0,0 +1,112 @@ +- op: replace + path: /paths/~1health~1alive + value: + get: + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is accepting incoming + HTTP requests. This status does currently not include checks whether the database connection is working. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the health status will never + refer to the cluster state, only to a single instance. + operationId: isAlive + responses: + '200': + content: + application/json: + schema: + required: + - status + type: object + properties: + status: + description: Always "ok". + type: string + description: '{{.ProjectHumanName}} is ready to accept connections.' + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1health~1ready + value: + get: + operationId: isReady + description: |- + This endpoint returns a HTTP 200 status code when {{.ProjectHumanName}} is up running and the environment dependencies (e.g. + the database) are responsive as well. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of {{.ProjectHumanName}}, the health status will never + refer to the cluster state, only to a single instance. + responses: + '200': + content: + application/json: + schema: + required: + - status + type: object + properties: + status: + description: Always "ok". + type: string + description: '{{.ProjectHumanName}} is ready to accept requests.' + '503': + content: + application/json: + schema: + required: + - errors + properties: + errors: + additionalProperties: + type: string + description: Errors contains a list of errors that caused the not ready status. + type: object + type: object + description: Ory Kratos is not yet ready to accept requests. + default: + content: + text/plain: + schema: + type: string + description: Unexpected error + summary: Check HTTP Server and Database Status + tags: {{ .HealthPathTags | toJson }} +- op: replace + path: /paths/~1version + value: + get: + description: |- + This endpoint returns the version of {{.ProjectHumanName}}. + + If the service supports TLS Edge Termination, this endpoint does not require the + `X-Forwarded-Proto` header to be set. + + Be aware that if you are running multiple nodes of this service, the version will never + refer to the cluster state, only to a single instance. + operationId: getVersion + responses: + '200': + content: + application/json: + schema: + type: object + required: + - version + properties: + version: + description: The version of {{.ProjectHumanName}}. + type: string + description: Returns the {{.ProjectHumanName}} version. + summary: Return Running Software Version. + tags: {{ .HealthPathTags | toJson }} diff --git a/oryx/httprouterx/router.go b/oryx/httprouterx/router.go new file mode 100644 index 0000000..03c982a --- /dev/null +++ b/oryx/httprouterx/router.go @@ -0,0 +1,164 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httprouterx + +import ( + "context" + "net/http" + "path" + "strings" +) + +const ( + AdminPrefix = "/admin" + + afterMatchHooks contextKey = 1 +) + +type ( + contextKey int + AfterMatchHookFunc func(*http.Request) + + router struct { + mux *http.ServeMux + prefix string + } + RouterAdmin struct{ router } + RouterPublic struct{ router } + + Router interface { + http.Handler + + Handle(pattern string, handler http.Handler) + HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) + + GET(route string, handle http.HandlerFunc) + OPTIONS(route string, handle http.HandlerFunc) + HEAD(route string, handle http.HandlerFunc) + POST(route string, handle http.HandlerFunc) + PUT(route string, handle http.HandlerFunc) + PATCH(route string, handle http.HandlerFunc) + DELETE(route string, handle http.HandlerFunc) + } +) + +func newRouter() *router { return &router{mux: http.NewServeMux()} } + +// NewRouter creates a new general purpose router. It should only be used when neither the admin nor the public router is applicable. +func NewRouter() Router { return newRouter() } + +// NewRouterAdmin creates a new admin router. +func NewRouterAdmin() *RouterAdmin { + return &RouterAdmin{router: *newRouter()} +} + +func (r *RouterAdmin) ToPublic() *RouterPublic { + return &RouterPublic{router: router{ + mux: r.mux, + prefix: "", // do not copy the admin prefix + }} +} + +// NewRouterPublic returns a public router. +func NewRouterPublic() *RouterPublic { + return &RouterPublic{router: *newRouter()} +} + +// NewRouterAdminWithPrefix creates a new router with the admin prefix. +func NewRouterAdminWithPrefix() *RouterAdmin { + r := NewRouterAdmin() + r.prefix = AdminPrefix + return r +} + +func (r *router) GET(route string, handle http.HandlerFunc) { + r.handle(http.MethodGet, route, handle) +} + +func (r *router) OPTIONS(route string, handle http.HandlerFunc) { + r.handle(http.MethodOptions, route, handle) +} + +func (r *router) HEAD(route string, handle http.HandlerFunc) { + r.handle(http.MethodHead, route, handle) +} + +func (r *router) POST(route string, handle http.HandlerFunc) { + r.handle(http.MethodPost, route, handle) +} + +func (r *router) PUT(route string, handle http.HandlerFunc) { + r.handle(http.MethodPut, route, handle) +} + +func (r *router) PATCH(route string, handle http.HandlerFunc) { + r.handle(http.MethodPatch, route, handle) +} + +func (r *router) DELETE(route string, handle http.HandlerFunc) { + r.handle(http.MethodDelete, route, handle) +} + +func (r *router) handle(method string, route string, handler http.Handler) { + route = path.Join(r.prefix, route) + r.mux.Handle(method+" "+route, afterMatchHookExecutor(handler)) +} + +func afterMatchHookExecutor(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if hooks, ok := r.Context().Value(afterMatchHooks).([]AfterMatchHookFunc); ok { + for _, hook := range hooks { + hook(r) + } + } + h.ServeHTTP(w, r) + }) +} + +// Handle registers the handler for the given pattern. It does not prepend the admin prefix, so the caller is responsible for ensuring that the pattern is correct. +func (r *router) Handle(pattern string, handler http.Handler) { + r.mux.Handle(pattern, afterMatchHookExecutor(handler)) +} + +// HandleFunc registers the handler for the given pattern. It does not prepend the admin prefix, so the caller is responsible for ensuring that the pattern is correct. +func (r *router) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { + r.Handle(pattern, http.HandlerFunc(handler)) +} + +func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) { r.mux.ServeHTTP(w, req) } + +func TrimTrailingSlashNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + r.URL.Path = strings.TrimSuffix(r.URL.Path, "/") + + next(rw, r) +} + +func NoCacheNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.Method == "GET" { + rw.Header().Set("Cache-Control", "private, no-cache, no-store, must-revalidate") + } + + next(rw, r) +} + +func AddAdminPrefixIfNotPresentNegroni(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if !strings.HasPrefix(r.URL.Path, AdminPrefix) { + r.URL.Path = path.Join(AdminPrefix, r.URL.Path) + if r.URL.RawPath != "" { + r.URL.RawPath = path.Join(AdminPrefix, r.URL.RawPath) + } + } + + next(rw, r) +} + +// WithAfterMatchHook adds the passed AfterMatchHookFunc to the request's context. The hook is called after the +// router matched the request, but before it is passed to the handler. This allows middlewares to access the matched +// pattern and other information that is only available after the router has matched the request. +// Note that this hook is called multiple times per request if routers are nested, the hook should anticipate that. +func WithAfterMatchHook(req *http.Request, newHooks ...AfterMatchHookFunc) *http.Request { + hooks, _ := req.Context().Value(afterMatchHooks).([]AfterMatchHookFunc) + ctx := context.WithValue(req.Context(), afterMatchHooks, append(hooks, newHooks...)) + return req.WithContext(ctx) +} diff --git a/oryx/httpx/assert.go b/oryx/httpx/assert.go new file mode 100644 index 0000000..c913267 --- /dev/null +++ b/oryx/httpx/assert.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "net/http" +) + +func GetResponseMeta(w http.ResponseWriter) (status, size int) { + switch t := w.(type) { + case interface{ Status() int }: + status = t.Status() + } + + switch t := w.(type) { + case interface{ Size() int }: + size = t.Size() + case interface{ Written() int64 }: + size = int(t.Written()) + } + + return +} diff --git a/oryx/httpx/chan_handler.go b/oryx/httpx/chan_handler.go new file mode 100644 index 0000000..42b9a20 --- /dev/null +++ b/oryx/httpx/chan_handler.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import "net/http" + +type chanHandler <-chan http.HandlerFunc + +var _ http.Handler = chanHandler(nil) + +func (c chanHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + (<-c)(w, r) +} + +// NewChanHandler returns a new handler and corresponding channel for sending handler funcs. +// Useful for testing. The argument buf specifies the channel capacity, so pass 0 for a sync handler. +func NewChanHandler(buf int) (http.Handler, chan<- http.HandlerFunc) { + c := make(chan http.HandlerFunc, buf) + return chanHandler(c), c +} diff --git a/oryx/httpx/client_info.go b/oryx/httpx/client_info.go new file mode 100644 index 0000000..1f966e4 --- /dev/null +++ b/oryx/httpx/client_info.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "net" + "net/http" + "strconv" + "strings" +) + +type GeoLocation struct { + City string + Region string + Country string + Latitude *float64 + Longitude *float64 +} + +func GetClientIPAddressesWithoutInternalIPs(ipAddresses []string) (string, error) { + var res string + + for i := len(ipAddresses) - 1; i >= 0; i-- { + ip := strings.TrimSpace(ipAddresses[i]) + + if !net.ParseIP(ip).IsPrivate() { + res = ip + break + } + } + + return res, nil +} + +func ClientIP(r *http.Request) string { + if trueClientIP := r.Header.Get("True-Client-IP"); trueClientIP != "" { + return trueClientIP + } else if cfConnectingIP := r.Header.Get("Cf-Connecting-IP"); cfConnectingIP != "" { + return cfConnectingIP + } else if realClientIP := r.Header.Get("X-Real-IP"); realClientIP != "" { + return realClientIP + } else if forwardedIP := r.Header.Get("X-Forwarded-For"); forwardedIP != "" { + ip, _ := GetClientIPAddressesWithoutInternalIPs(strings.Split(forwardedIP, ",")) + return ip + } else { + return r.RemoteAddr + } +} + +func parseFloatHeaderValue(headerValue string) *float64 { + if headerValue == "" { + return nil + } + + val, err := strconv.ParseFloat(headerValue, 64) + if err != nil { + return nil + } + + return &val +} + +func ClientGeoLocation(r *http.Request) *GeoLocation { + return &GeoLocation{ + City: r.Header.Get("Cf-Ipcity"), + Region: r.Header.Get("Cf-Region-Code"), + Country: r.Header.Get("Cf-Ipcountry"), + Longitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplongitude")), + Latitude: parseFloatHeaderValue(r.Header.Get("Cf-Iplatitude")), + } +} diff --git a/oryx/httpx/content_type.go b/oryx/httpx/content_type.go new file mode 100644 index 0000000..6c01c9f --- /dev/null +++ b/oryx/httpx/content_type.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "mime" + "net/http" + "slices" + "strings" +) + +// HasContentType determines whether the request `content-type` includes a +// server-acceptable mime-type +// +// Failure should yield an HTTP 415 (`http.StatusUnsupportedMediaType`) +func HasContentType(r *http.Request, mimetypes ...string) bool { + contentType := r.Header.Get("Content-Type") + if contentType == "" { + return slices.Contains(mimetypes, "application/octet-stream") + } + + mediaType, _, err := mime.ParseMediaType(strings.TrimSpace(contentType)) + if err != nil { + return false + } + return slices.Contains(mimetypes, mediaType) +} diff --git a/oryx/httpx/gzip_server.go b/oryx/httpx/gzip_server.go new file mode 100644 index 0000000..1ee6603 --- /dev/null +++ b/oryx/httpx/gzip_server.go @@ -0,0 +1,50 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "compress/gzip" + "fmt" + "io" + "net/http" + "strings" +) + +type CompressionRequestReader struct { + ErrHandler func(w http.ResponseWriter, r *http.Request, err error) +} + +func defaultCompressionErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) +} + +func NewCompressionRequestReader(eh func(w http.ResponseWriter, r *http.Request, err error)) *CompressionRequestReader { + if eh == nil { + eh = defaultCompressionErrorHandler + } + + return &CompressionRequestReader{ + ErrHandler: eh, + } +} + +func (c *CompressionRequestReader) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + for _, enc := range strings.Split(r.Header.Get("Content-Encoding"), ",") { + switch enc = strings.TrimSpace(enc); enc { + case "gzip": + reader, err := gzip.NewReader(r.Body) + if err != nil { + c.ErrHandler(w, r, err) + return + } + r.Body = io.NopCloser(reader) + case "identity", "": + // nothing to do + default: + c.ErrHandler(w, r, fmt.Errorf("%s content encoding not supported", enc)) + } + } + + next(w, r) +} diff --git a/oryx/httpx/request.go b/oryx/httpx/request.go new file mode 100644 index 0000000..b18d1a2 --- /dev/null +++ b/oryx/httpx/request.go @@ -0,0 +1,51 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// NewRequestJSON returns a new JSON *http.Request. +func NewRequestJSON(method, url string, data interface{}) (*http.Request, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(data); err != nil { + return nil, errors.WithStack(err) + } + req, err := http.NewRequest(method, url, &b) + if err != nil { + return nil, errors.WithStack(err) + } + req.Header.Set("Content-Type", "application/json") + return req, nil +} + +// NewRequestForm returns a new POST Form *http.Request. +func NewRequestForm(method, url string, data url.Values) (*http.Request, error) { + req, err := http.NewRequest(method, url, strings.NewReader(data.Encode())) + if err != nil { + return nil, errors.WithStack(err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + return req, nil +} + +// MustNewRequest returns a new *http.Request or fatals. +func MustNewRequest(method, url string, body io.Reader, contentType string) *http.Request { + req, err := http.NewRequest(method, url, body) + if err != nil { + panic(err) + } + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + return req +} diff --git a/oryx/httpx/resilient_client.go b/oryx/httpx/resilient_client.go new file mode 100644 index 0000000..7d87323 --- /dev/null +++ b/oryx/httpx/resilient_client.go @@ -0,0 +1,140 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "github.com/hashicorp/go-retryablehttp" + "golang.org/x/oauth2" + + "github.com/ory/herodot" + "github.com/ory/x/logrusx" +) + +type resilientOptions struct { + c *http.Client + l interface{} + retryWaitMin time.Duration + retryWaitMax time.Duration + retryMax int + noInternalIPs bool + internalIPExceptions []string +} + +func newResilientOptions() *resilientOptions { + return &resilientOptions{ + c: &http.Client{Timeout: time.Minute}, + retryWaitMin: 1 * time.Second, + retryWaitMax: 30 * time.Second, + retryMax: 4, + l: log.New(io.Discard, "", log.LstdFlags), + } +} + +// ResilientOptions is a set of options for the ResilientClient. +type ResilientOptions func(o *resilientOptions) + +// ResilientClientWithMaxRetry sets the maximum number of retries. +func ResilientClientWithMaxRetry(retryMax int) ResilientOptions { + return func(o *resilientOptions) { o.retryMax = retryMax } +} + +// ResilientClientWithMinRetryWait sets the minimum wait time between retries. +func ResilientClientWithMinRetryWait(retryWaitMin time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.retryWaitMin = retryWaitMin } +} + +// ResilientClientWithMaxRetryWait sets the maximum wait time for a retry. +func ResilientClientWithMaxRetryWait(retryWaitMax time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.retryWaitMax = retryWaitMax } +} + +// ResilientClientWithConnectionTimeout sets the connection timeout for the client. +func ResilientClientWithConnectionTimeout(connTimeout time.Duration) ResilientOptions { + return func(o *resilientOptions) { o.c.Timeout = connTimeout } +} + +// ResilientClientWithLogger sets the logger to be used by the client. +func ResilientClientWithLogger(l *logrusx.Logger) ResilientOptions { + return func(o *resilientOptions) { o.l = l } +} + +// ResilientClientDisallowInternalIPs disallows internal IPs from being used. +func ResilientClientDisallowInternalIPs() ResilientOptions { + return func(o *resilientOptions) { o.noInternalIPs = true } +} + +// ResilientClientAllowInternalIPRequestsTo allows requests to the glob-matching URLs even +// if they are internal IPs. +func ResilientClientAllowInternalIPRequestsTo(urlGlobs ...string) ResilientOptions { + return func(o *resilientOptions) { o.internalIPExceptions = urlGlobs } +} + +// ResilientClientNoFollowRedirects configures the client to not follow redirects. +func ResilientClientNoFollowRedirects() ResilientOptions { + return func(o *resilientOptions) { + o.c.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // don't follow redirects + } + } +} + +// NewResilientClient creates a new ResilientClient. +func NewResilientClient(opts ...ResilientOptions) *retryablehttp.Client { + o := newResilientOptions() + for _, f := range opts { + f(o) + } + + if o.noInternalIPs { + o.c.Transport = &noInternalIPRoundTripper{ + onWhitelist: allowInternalAllowIPv6, + notOnWhitelist: prohibitInternalAllowIPv6, + internalIPExceptions: o.internalIPExceptions, + } + } else { + o.c.Transport = allowInternalAllowIPv6 + } + + cl := retryablehttp.NewClient() + cl.HTTPClient = o.c + cl.Logger = o.l + cl.RetryWaitMin = o.retryWaitMin + cl.RetryWaitMax = o.retryWaitMax + cl.RetryMax = o.retryMax + cl.CheckRetry = retryablehttp.DefaultRetryPolicy + cl.Backoff = retryablehttp.DefaultBackoff + return cl +} + +// SetOAuth2 modifies the given client to enable OAuth2 authentication. Requests +// with the client should always use the returned context. +// +// client := http.NewResilientClient(opts...) +// ctx, client = httpx.SetOAuth2(ctx, client, oauth2Config, oauth2Token) +// req, err := retryablehttp.NewRequestWithContext(ctx, ...) +// if err != nil { /* ... */ } +// res, err := client.Do(req) +func SetOAuth2(ctx context.Context, cl *retryablehttp.Client, c OAuth2Config, t *oauth2.Token) (context.Context, *retryablehttp.Client) { + ctx = context.WithValue(ctx, oauth2.HTTPClient, cl.HTTPClient) + cl.HTTPClient = c.Client(ctx, t) + return ctx, cl +} + +type ( + OAuth2Config interface { + Client(context.Context, *oauth2.Token) *http.Client + } + ClientProvider interface { + HTTPClient(ctx context.Context, opts ...ResilientOptions) *retryablehttp.Client + } + WriterProvider interface { + Writer() herodot.Writer + } +) diff --git a/oryx/httpx/ssrf.go b/oryx/httpx/ssrf.go new file mode 100644 index 0000000..cfa52b9 --- /dev/null +++ b/oryx/httpx/ssrf.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "net" + "net/http" + "net/http/httptrace" + "time" + + "github.com/gobwas/glob" + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/ory/x/ipx" +) + +var _ http.RoundTripper = (*noInternalIPRoundTripper)(nil) + +type noInternalIPRoundTripper struct { + onWhitelist, notOnWhitelist http.RoundTripper + internalIPExceptions []string +} + +// RoundTrip implements http.RoundTripper. +func (n noInternalIPRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + incoming := IncomingRequestURL(request) + incoming.RawQuery = "" + incoming.RawFragment = "" + for _, exception := range n.internalIPExceptions { + compiled, err := glob.Compile(exception, '.', '/') + if err != nil { + return nil, err + } + if compiled.Match(incoming.String()) { + return n.onWhitelist.RoundTrip(request) + } + } + + return n.notOnWhitelist.RoundTrip(request) +} + +var ( + prohibitInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.ProhibitInternalDialFunc)) + allowInternalAllowIPv6 http.RoundTripper = OTELTraceTransport(ssrfTransport(ipx.AllowInternalDialFunc)) +) + +func ssrfTransport(dial func(ctx context.Context, network, address string) (net.Conn, error)) *http.Transport { + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: dial, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} + +// OTELTraceTransport wraps the given http.Transport with OpenTelemetry instrumentation. +func OTELTraceTransport(t *http.Transport) http.RoundTripper { + return otelhttp.NewTransport(t, otelhttp.WithClientTrace(func(ctx context.Context) *httptrace.ClientTrace { + return otelhttptrace.NewClientTrace(ctx, otelhttptrace.WithoutHeaders(), otelhttptrace.WithoutSubSpans()) + })) +} diff --git a/oryx/httpx/transports.go b/oryx/httpx/transports.go new file mode 100644 index 0000000..bb28134 --- /dev/null +++ b/oryx/httpx/transports.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import "net/http" + +// WrapTransportWithHeader wraps a http.Transport to always use the values from the given header. +func WrapTransportWithHeader(parent http.RoundTripper, h http.Header) *TransportWithHeader { + return &TransportWithHeader{ + RoundTripper: parent, + h: h, + } +} + +// NewTransportWithHeader returns a new http.Transport that always uses the values from the given header. +func NewTransportWithHeader(h http.Header) *TransportWithHeader { + return &TransportWithHeader{ + RoundTripper: http.DefaultTransport, + h: h, + } +} + +// TransportWithHeader is an http.RoundTripper that always uses the values from the given header. +type TransportWithHeader struct { + http.RoundTripper + h http.Header +} + +// RoundTrip implements http.RoundTripper. +func (ct *TransportWithHeader) RoundTrip(req *http.Request) (*http.Response, error) { + for k := range ct.h { + req.Header.Set(k, ct.h.Get(k)) + } + return ct.RoundTripper.RoundTrip(req) +} + +// NewTransportWithHost returns a new http.Transport that always uses the given host. +func NewTransportWithHost(host string) *TransportWithHost { + return &TransportWithHost{ + RoundTripper: http.DefaultTransport, + host: host, + } +} + +// WrapRoundTripperWithHost wraps a http.RoundTripper that always uses the given host. +func WrapRoundTripperWithHost(parent http.RoundTripper, host string) *TransportWithHost { + return &TransportWithHost{ + RoundTripper: parent, + host: host, + } +} + +// TransportWithHost is an http.RoundTripper that always uses the given host. +type TransportWithHost struct { + http.RoundTripper + host string +} + +// RoundTrip implements http.RoundTripper. +func (ct *TransportWithHost) RoundTrip(req *http.Request) (*http.Response, error) { + req.Host = ct.host + return ct.RoundTripper.RoundTrip(req) +} + +type TransportFunc func(*http.Request) (*http.Response, error) + +func (f TransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } diff --git a/oryx/httpx/url.go b/oryx/httpx/url.go new file mode 100644 index 0000000..ff206e9 --- /dev/null +++ b/oryx/httpx/url.go @@ -0,0 +1,29 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "cmp" + "net/http" + "net/url" +) + +// IncomingRequestURL returns the URL of the incoming HTTP request by looking at the host, TLS, and X-Forwarded-* headers. +func IncomingRequestURL(r *http.Request) *url.URL { + source := *r.URL + source.Host = cmp.Or(source.Host, r.Header.Get("X-Forwarded-Host"), r.Host) + + if proto := r.Header.Get("X-Forwarded-Proto"); len(proto) > 0 { + source.Scheme = proto + } + + if source.Scheme == "" { + source.Scheme = "https" + if r.TLS == nil { + source.Scheme = "http" + } + } + + return &source +} diff --git a/oryx/httpx/wait_for.go b/oryx/httpx/wait_for.go new file mode 100644 index 0000000..c85f5d2 --- /dev/null +++ b/oryx/httpx/wait_for.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package httpx + +import ( + "context" + "io" + "net/http" + "time" + + "github.com/avast/retry-go/v4" + "github.com/pkg/errors" + "github.com/tidwall/gjson" +) + +// WaitForEndpoint waits for the endpoint to be available. +func WaitForEndpoint(ctx context.Context, endpoint string, opts ...retry.Option) error { + return WaitForEndpointWithClient(ctx, http.DefaultClient, endpoint, opts...) +} + +// WaitForEndpointWithClient waits for the endpoint to be available while using the given http.Client. +func WaitForEndpointWithClient(ctx context.Context, client *http.Client, endpoint string, opts ...retry.Option) error { + return retry.Do(func() error { + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + return err + } + + res, err := client.Do(req) + if err != nil { + return err + } + defer func() { + _ = res.Body.Close() + }() + + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + if gjson.GetBytes(body, "status").String() != "ok" { + return errors.Errorf("status is not yet ok: %s", body) + } + + return nil + }, + append([]retry.Option{ + retry.DelayType(retry.BackOffDelay), + retry.Delay(time.Second), + retry.MaxDelay(time.Second * 2), + retry.Attempts(20), + }, opts...)...) +} diff --git a/oryx/ioutilx/pkger.go b/oryx/ioutilx/pkger.go new file mode 100644 index 0000000..ebd2012 --- /dev/null +++ b/oryx/ioutilx/pkger.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ioutilx + +import ( + "io" +) + +// MustReadAll reads a reader or panics. +func MustReadAll(r io.Reader) []byte { + all, err := io.ReadAll(r) + if err != nil { + panic(err) + } + return all +} diff --git a/oryx/ipx/cidr.go b/oryx/ipx/cidr.go new file mode 100644 index 0000000..341e792 --- /dev/null +++ b/oryx/ipx/cidr.go @@ -0,0 +1,23 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "iter" + "net/netip" +) + +func Hosts(prefix netip.Prefix) iter.Seq[netip.Addr] { + prefix = prefix.Masked() + return func(yield func(netip.Addr) bool) { + if !prefix.IsValid() { + return + } + for addr := prefix.Addr().Next(); prefix.Contains(addr); addr = addr.Next() { + if !yield(addr) { + return + } + } + } +} diff --git a/oryx/ipx/ip_validator.go b/oryx/ipx/ip_validator.go new file mode 100644 index 0000000..d7d88b7 --- /dev/null +++ b/oryx/ipx/ip_validator.go @@ -0,0 +1,128 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "context" + stderrors "errors" + "net" + "net/netip" + "net/url" + "time" + + "code.dny.dev/ssrf" + "golang.org/x/sync/errgroup" + + "github.com/pkg/errors" +) + +// IsAssociatedIPAllowedWhenSet is a wrapper for IsAssociatedIPAllowed which returns valid +// when ipOrHostnameOrURL is empty. +func IsAssociatedIPAllowedWhenSet(ctx context.Context, ipOrHostnameOrURL string) error { + if ipOrHostnameOrURL == "" { + return nil + } + return IsAssociatedIPAllowed(ctx, ipOrHostnameOrURL) +} + +// AreAllAssociatedIPsAllowed fails if one of the pairs is failing. +func AreAllAssociatedIPsAllowed(ctx context.Context, pairs map[string]string) error { + g, ctx := errgroup.WithContext(ctx) + for key, ipOrHostnameOrURL := range pairs { + g.Go(func() error { + return errors.Wrapf(IsAssociatedIPAllowed(ctx, ipOrHostnameOrURL), "key %s validation is failing", key) + }) + } + return g.Wait() +} + +// IsAssociatedIPAllowed returns nil for a domain (with NS lookup), IP, or IPv6 address if it +// does not resolve to a private IP subnet. This is a first level of defense against +// SSRF attacks by disallowing any domain or IP to resolve to a private network range. +// +// Please keep in mind that validations for domains is valid only when looking up. +// A malicious actor could easily update the DSN record post validation to point +// to an internal IP +func IsAssociatedIPAllowed(ctx context.Context, ipOrHostnameOrURL string) error { + ipOrHostname := ipOrHostnameOrURL + if parsed, err := url.ParseRequestURI(ipOrHostnameOrURL); err == nil { + ipOrHostname = parsed.Hostname() + } + + if ip, err := netip.ParseAddr(ipOrHostname); err == nil { + if !allowed(ip) { + return errors.Errorf("ip %s is not a permitted destination", ip) + } + return nil + } + + if addr, err := netip.ParseAddrPort(ipOrHostnameOrURL); err == nil { + if !allowed(addr.Addr()) { + return errors.Errorf("ip %s is not a permitted destination", addr.Addr()) + } + return nil + } + + ctx, cancel := context.WithTimeoutCause(ctx, 2*time.Second, errors.New("DNS lookup timed out")) + defer cancel() + ips, err := resolver.LookupNetIP(ctx, "ip", ipOrHostname) + if err != nil { + if dnsErr, ok := stderrors.AsType[*net.DNSError](err); ok { + // Copy the `*net.DNSError` before masking `Server` to avoid a data + // race: the DNS resolver uses `singleflight` to deduplicate + // concurrent lookups, so multiple goroutines may receive the same + // `*net.DNSError` pointer. Mutating it in place races with concurrent + // readers (e.g. the `otelhttp` `dnsDone` trace hook). + maskedDNS := *dnsErr + maskedDNS.Server = "" // Mask our DNS server's IP address. + return errors.Wrapf(&maskedDNS, "failed to resolve %s", ipOrHostnameOrURL) + } + } + + for _, ip := range ips { + if !allowed(ip) { + return errors.Wrapf(&net.DNSError{ + Err: "no such host", + Name: ipOrHostname, + Server: "", + IsTimeout: false, + IsTemporary: false, + IsNotFound: true, + }, "failed to resolve %s", ipOrHostnameOrURL) + } + } + + return nil +} + +var resolver = &net.Resolver{PreferGo: true} + +func allowed(ip netip.Addr) bool { + if !ip.IsGlobalUnicast() { + return false + } + + if ip.Is4() { + for _, net := range ssrf.IPv4DeniedPrefixes { + if net.Contains(ip) { + return false + } + } + } else { // IPv6 + // ip.IsGlobalUnicast returns true for IPv6 addresses which fall outside + // of the current IANA-allocated 2000::/3 global unicast space. Hence, + // we need to check for ourselves if the IPv6 address is within the + // currently allocated block. + if !ssrf.IPv6GlobalUnicast.Contains(ip) { + return false + } + for _, net := range ssrf.IPv6DeniedPrefixes { + if net.Contains(ip) { + return false + } + } + } + + return true +} diff --git a/oryx/ipx/ssrf.go b/oryx/ipx/ssrf.go new file mode 100644 index 0000000..38ea4c8 --- /dev/null +++ b/oryx/ipx/ssrf.go @@ -0,0 +1,100 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package ipx + +import ( + "context" + "errors" + "net" + "net/netip" + "time" + + "code.dny.dev/ssrf" +) + +var ( + ProhibitInternalDialFunc = ssrfDialFunc( + ssrf.WithAnyPort(), + ssrf.WithNetworks("tcp4", "tcp6"), + ) + + AllowInternalDialFunc = ssrfDialFunc( + ssrf.WithAnyPort(), + ssrf.WithNetworks("tcp4", "tcp6"), + ssrf.WithAllowedV4Prefixes( + netip.MustParsePrefix("10.0.0.0/8"), // Private-Use (RFC 1918) + netip.MustParsePrefix("127.0.0.0/8"), // Loopback (RFC 1122, Section 3.2.1.3)) + netip.MustParsePrefix("169.254.0.0/16"), // Link Local (RFC 3927) + netip.MustParsePrefix("172.16.0.0/12"), // Private-Use (RFC 1918) + netip.MustParsePrefix("192.168.0.0/16"), // Private-Use (RFC 1918) + ), + ssrf.WithAllowedV6Prefixes( + netip.MustParsePrefix("::1/128"), // Loopback (RFC 4193) + netip.MustParsePrefix("fc00::/7"), // Unique Local (RFC 4193) + ), + ) +) + +func ssrfDialFunc(opt ...ssrf.Option) func(ctx context.Context, network, address string) (net.Conn, error) { + dialer := net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } + dialer.Control = ssrf.New(opt...).Safe + return func(ctx context.Context, network, address string) (net.Conn, error) { + c, err := dialer.DialContext(ctx, network, address) + if err == nil { + return c, nil + } + + if dnsErr, ok := errors.AsType[*net.DNSError](err); ok { + // Copy the `*net.DNSError` before masking `Server` to avoid a data + // race: the DNS resolver uses `singleflight` to deduplicate + // concurrent lookups, so multiple goroutines may receive the same + // `*net.DNSError` pointer. Mutating it in place races with concurrent + // readers (e.g. the `otelhttp` `dnsDone` trace hook). + maskedDNS := *dnsErr + maskedDNS.Server = "" // Mask our DNS server's IP address. + // Also copy the outer `*net.OpError` (if present) to preserve the + // full error chain that callers depend on, replacing its `Err` field + // with the masked copy. + // Surprisingly Go does not have a good way of manipulating error chains. + if opErr, ok := errors.AsType[*net.OpError](err); ok { + maskedOp := *opErr + maskedOp.Err = &maskedDNS + return nil, &maskedOp + } + return nil, &maskedDNS + } + + if !errors.Is(err, ssrf.ErrProhibitedIP) { + return nil, err + } + + host, _, _ := net.SplitHostPort(address) + _, addrErr := netip.ParseAddrPort(address) + if addrErr != nil { + // We were given a DNS name: the error we return must look like a DNS error. + return nil, &net.OpError{ + Op: "dial", + Net: network, + Addr: nil, + Err: &net.DNSError{ + Err: "no such host", + Name: host, + Server: "", + IsTimeout: false, + IsTemporary: false, + IsNotFound: true, + }, + } + } + return nil, &net.OpError{ + Op: "dial", + Net: network, + Addr: nil, + Err: errors.New("no route to host"), + } + } +} diff --git a/oryx/josex/encoding.go b/oryx/josex/encoding.go new file mode 100644 index 0000000..57ff0e4 --- /dev/null +++ b/oryx/josex/encoding.go @@ -0,0 +1,55 @@ +/*- + * Copyright 2019 Square Inc. + * + * 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. + */ + +package josex + +import "io" + +// Base64Reader wraps an input stream consisting of either standard or url-safe +// base64 data, and maps it to a raw (unpadded) standard encoding. This can be used +// to read any base64-encoded data as input, whether padded, unpadded, standard or +// url-safe. +type Base64Reader struct { + In io.Reader +} + +func (r Base64Reader) Read(p []byte) (n int, err error) { + n, err = r.In.Read(p) + if err != nil { + return + } + + for i := range n { + switch p[i] { + // Map - to + + case 0x2D: + p[i] = 0x2B + // Map _ to / + case 0x5F: + p[i] = 0x2F + // Strip = + case 0x3D: + n = i + default: + } + } + + if n == 0 { + err = io.EOF + } + + return +} diff --git a/oryx/josex/generate.go b/oryx/josex/generate.go new file mode 100644 index 0000000..055a4c5 --- /dev/null +++ b/oryx/josex/generate.go @@ -0,0 +1,121 @@ +/*- + * Copyright 2019 Square Inc. + * + * 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. + */ + +package josex + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "errors" + "fmt" + + "github.com/go-jose/go-jose/v3" +) + +// NewSigningKey generates a keypair for corresponding SignatureAlgorithm. +func NewSigningKey(alg jose.SignatureAlgorithm, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch alg { + case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA: + keylen := map[jose.SignatureAlgorithm]int{ + jose.ES256: 256, + jose.ES384: 384, + jose.ES512: 521, // sic! + jose.EdDSA: 256, + } + if bits != 0 && bits != keylen[alg] { + return nil, nil, errors.New("invalid elliptic curve key size, this algorithm does not support arbitrary size") + } + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, nil, errors.New("invalid key size for RSA key, 2048 or more is required") + } + } + switch alg { + case jose.ES256: + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ES384: + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ES512: + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.EdDSA: + pub, key, err := ed25519.GenerateKey(rand.Reader) + return pub, key, err + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + default: + return nil, nil, fmt.Errorf("unknown algorithm %s for signing key", alg) + } +} + +// NewEncryptionKey generates a keypair for corresponding KeyAlgorithm. +func NewEncryptionKey(alg jose.KeyAlgorithm, bits int) (crypto.PublicKey, crypto.PrivateKey, error) { + switch alg { + case jose.RSA1_5, jose.RSA_OAEP, jose.RSA_OAEP_256: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, nil, errors.New("invalid key size for RSA key, 2048 or more is required") + } + key, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + case jose.ECDH_ES, jose.ECDH_ES_A128KW, jose.ECDH_ES_A192KW, jose.ECDH_ES_A256KW: + var crv elliptic.Curve + switch bits { + case 0, 256: + crv = elliptic.P256() + case 384: + crv = elliptic.P384() + case 521: + crv = elliptic.P521() + default: + return nil, nil, errors.New("invalid elliptic curve key size, use one of 256, 384, or 521") + } + key, err := ecdsa.GenerateKey(crv, rand.Reader) + if err != nil { + return nil, nil, err + } + return key.Public(), key, err + default: + return nil, nil, fmt.Errorf("unknown algorithm %s for encryption key", alg) + } +} diff --git a/oryx/josex/public.go b/oryx/josex/public.go new file mode 100644 index 0000000..667a2cb --- /dev/null +++ b/oryx/josex/public.go @@ -0,0 +1,29 @@ +package josex + +import ( + "crypto" + + "github.com/go-jose/go-jose/v3" +) + +// ToPublicKey returns the public key of the given private key. +func ToPublicKey(k *jose.JSONWebKey) jose.JSONWebKey { + if key := k.Public(); key.Key != nil { + return key + } + + // HSM workaround - jose does not understand crypto.Signer / HSM so we need to manually + // extract the public key. + switch key := k.Key.(type) { + case crypto.Signer: + newKey := *k + newKey.Key = key.Public() + return newKey + case jose.OpaqueSigner: + newKey := *k + newKey.Key = key.Public().Key + return newKey + } + + return jose.JSONWebKey{} +} diff --git a/oryx/josex/utils.go b/oryx/josex/utils.go new file mode 100644 index 0000000..036f36a --- /dev/null +++ b/oryx/josex/utils.go @@ -0,0 +1,103 @@ +/*- + * Copyright 2019 Square Inc. + * + * 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. + */ + +package josex + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "fmt" + + "github.com/go-jose/go-jose/v3" +) + +// LoadJSONWebKey returns a *jose.JSONWebKey for a given JSON string. +func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) { + var jwk jose.JSONWebKey + err := jwk.UnmarshalJSON(json) + if err != nil { + return nil, err + } + if !jwk.Valid() { + return nil, errors.New("invalid JWK key") + } + if jwk.IsPublic() != pub { + return nil, errors.New("priv/pub JWK key mismatch") + } + return &jwk, nil +} + +// LoadPublicKey loads a public key from PEM/DER/JWK-encoded data. +func LoadPublicKey(data []byte) (interface{}, error) { + input := data + + block, _ := pem.Decode(data) + if block != nil { + input = block.Bytes + } + + // Try to load SubjectPublicKeyInfo + pub, err0 := x509.ParsePKIXPublicKey(input) + if err0 == nil { + return pub, nil + } + + cert, err1 := x509.ParseCertificate(input) + if err1 == nil { + return cert.PublicKey, nil + } + + jwk, err2 := LoadJSONWebKey(data, true) + if err2 == nil { + return jwk, nil + } + + return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s' and '%s'", err0, err1, err2) +} + +// LoadPrivateKey loads a private key from PEM/DER/JWK-encoded data. +func LoadPrivateKey(data []byte) (interface{}, error) { + input := data + + block, _ := pem.Decode(data) + if block != nil { + input = block.Bytes + } + + var priv interface{} + priv, err0 := x509.ParsePKCS1PrivateKey(input) + if err0 == nil { + return priv, nil + } + + priv, err1 := x509.ParsePKCS8PrivateKey(input) + if err1 == nil { + return priv, nil + } + + priv, err2 := x509.ParseECPrivateKey(input) + if err2 == nil { + return priv, nil + } + + jwk, err3 := LoadJSONWebKey(input, false) + if err3 == nil { + return jwk, nil + } + + return nil, fmt.Errorf("square/go-jose: parse error, got '%s', '%s', '%s' and '%s'", err0, err1, err2, err3) +} diff --git a/oryx/jsonnetsecure/cmd.go b/oryx/jsonnetsecure/cmd.go new file mode 100644 index 0000000..6b9cb63 --- /dev/null +++ b/oryx/jsonnetsecure/cmd.go @@ -0,0 +1,115 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "bufio" + "fmt" + "io" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/ory/x/landlockx" +) + +const ( + GiB uint64 = 1024 * 1024 * 1024 + // Generous limit on virtual memory including the peak memory allocated by the Go runtime, the Jsonnet VM, + // and the Jsonnet script. + // This number was acquired by running: + // Found by trial and error with: + // `ulimit -Sv 1048576 && echo '{"Snippet": "{user_id: std.repeat(\'a\', 1000)}"}' | kratos jsonnet -0` + // NOTE: Ideally we'd like to limit RSS but that is not possible on Linux with `ulimit/setrlimit(2)` - only with cgroups. + virtualMemoryLimitBytes = 2 * GiB +) + +func NewJsonnetCmd() *cobra.Command { + var null bool + cmd := &cobra.Command{ + Use: "jsonnet", + Short: "Run Jsonnet as a CLI command", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + // Lock the worker down to its already-open stdio: the + // only I/O it does is read snippets/parameters from stdin + // and write JSON to stdout/stderr. Denying all path-based + // filesystem access prevents a malicious snippet from + // touching the operator's file system even if the + // jsonnet VM ever exposed a path-import primitive. + if err := landlockx.ApplyEmpty(nil); err != nil { + return errors.Wrap(err, "failed to apply empty landlock sandbox") + } + + // This could fail because current limits are lower than what we tried to set, + // so we still continue in this case. + SetVirtualMemoryLimit(virtualMemoryLimitBytes) + + if null { + return scan(cmd.OutOrStdout(), cmd.InOrStdin()) + } + + input, err := io.ReadAll(cmd.InOrStdin()) + if err != nil { + return errors.Wrap(err, "failed to read from stdin") + } + + json, err := eval(input) + if err != nil { + return errors.Wrap(err, "failed to evaluate jsonnet") + } + + if _, err := io.WriteString(cmd.OutOrStdout(), json); err != nil { + return errors.Wrap(err, "failed to write json output") + } + return nil + }, + } + cmd.Flags().BoolVarP(&null, "null", "0", false, + `Read multiple snippets and parameters from stdin separated by null bytes. +Output will be in the same order as inputs, separated by null bytes. +Evaluation errors will also be reported to stdout, separated by null bytes. +Non-recoverable errors are written to stderr and the program will terminate with a non-zero exit code.`) + + return cmd +} + +func scan(w io.Writer, r io.Reader) error { + scanner := bufio.NewScanner(r) + scanner.Split(splitNull) + for scanner.Scan() { + json, err := eval(scanner.Bytes()) + if err != nil { + json = fmt.Sprintf("ERROR: %s", err) + } + if _, err := fmt.Fprintf(w, "%s%c", json, 0); err != nil { + return errors.Wrap(err, "failed to write json output") + } + } + return errors.Wrap(scanner.Err(), "failed to read from stdin") +} + +func eval(input []byte) (json string, err error) { + var params processParameters + if err := params.Decode(input); err != nil { + return "", err + } + + vm := MakeInProcessVM() + + for _, it := range params.ExtCodes { + vm.ExtCode(it.Key, it.Value) + } + for _, it := range params.ExtVars { + vm.ExtVar(it.Key, it.Value) + } + for _, it := range params.TLACodes { + vm.TLACode(it.Key, it.Value) + } + for _, it := range params.TLAVars { + vm.TLAVar(it.Key, it.Value) + } + + return vm.EvaluateAnonymousSnippet(params.Filename, params.Snippet) +} diff --git a/oryx/jsonnetsecure/cmd/root.go b/oryx/jsonnetsecure/cmd/root.go new file mode 100644 index 0000000..d6e017d --- /dev/null +++ b/oryx/jsonnetsecure/cmd/root.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/ory/x/jsonnetsecure" +) + +func main() { + if err := jsonnetsecure.NewJsonnetCmd().ExecuteContext(context.Background()); err != nil { + fmt.Println(err) + os.Exit(-1) + } +} diff --git a/oryx/jsonnetsecure/jsonnet.go b/oryx/jsonnetsecure/jsonnet.go new file mode 100644 index 0000000..8666cfd --- /dev/null +++ b/oryx/jsonnetsecure/jsonnet.go @@ -0,0 +1,153 @@ +package jsonnetsecure + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "path" + "runtime" + "testing" + + "github.com/google/go-jsonnet" + "github.com/pkg/errors" +) + +type ( + VM interface { + EvaluateAnonymousSnippet(filename string, snippet string) (json string, formattedErr error) + ExtCode(key string, val string) + ExtVar(key string, val string) + TLACode(key string, val string) + TLAVar(key string, val string) + } + + kv struct { + Key, Value string + } + processParameters struct { + Filename, Snippet string + TLACodes, TLAVars, ExtCodes, ExtVars []kv + } + + vmOptions struct { + jsonnetBinaryPath string + args []string + ctx context.Context + pool *pool + } + + Option func(o *vmOptions) +) + +func (pp *processParameters) EncodeTo(w io.Writer) error { + return json.NewEncoder(w).Encode(pp) +} + +func (pp *processParameters) Decode(d []byte) error { + return json.Unmarshal(d, pp) +} + +func newVMOptions() *vmOptions { + jsonnetBinaryPath, _ := os.Executable() + return &vmOptions{ + jsonnetBinaryPath: jsonnetBinaryPath, + ctx: context.Background(), + } +} + +func WithContext(ctx context.Context) Option { + return func(o *vmOptions) { + o.ctx = ctx + } +} + +func WithJsonnetBinary(jsonnetBinaryPath string) Option { + return func(o *vmOptions) { + o.jsonnetBinaryPath = jsonnetBinaryPath + } +} + +func WithProcessArgs(args ...string) Option { + return func(o *vmOptions) { + o.args = args + } +} + +// ErrNoProcessPool is returned by MakeSecureVM when called without a process +// pool. It is a distinct error because the alternative — quietly returning an +// in-process VM — would strip the isolation callers of this package rely on. +var ErrNoProcessPool = errors.New("jsonnetsecure: a process pool is required; use MakeInProcessVM to evaluate in this process without isolation") + +// MakeSecureVM returns a VM that evaluates snippets in a worker process taken +// from p, so that a snippet which exhausts memory, spins on the CPU, or crashes +// takes down only that worker. +// +// p is a required argument rather than an option because a VM without a pool +// offers no isolation at all. Passing a nil pool returns ErrNoProcessPool. +func MakeSecureVM(p Pool, opts ...Option) (VM, error) { + // A nil *pool inside a non-nil Pool interface is not reachable from outside + // this package (Pool has an unexported method), but check the concrete + // value anyway so a future in-package mistake cannot slip through. + concrete, _ := p.(*pool) + if p == nil || concrete == nil { + return nil, errors.WithStack(ErrNoProcessPool) + } + + options := newVMOptions() + for _, o := range opts { + o(options) + } + options.pool = concrete + + return NewProcessPoolVM(options), nil +} + +// MakeInProcessVM returns a Jsonnet VM that evaluates in the calling process +// with imports disabled. It provides no isolation: a malicious or buggy snippet +// can exhaust this process's memory and CPU, so it is only safe for trusted +// input. The two legitimate uses are the jsonnet subcommand, which is itself +// the isolation boundary, and offline CLI linting. Everything that evaluates +// tenant-supplied Jsonnet must use MakeSecureVM. +func MakeInProcessVM() *jsonnet.VM { + vm := jsonnet.MakeVM() + vm.Importer(new(ErrorImporter)) + return vm +} + +// ErrorImporter errors when calling "import". +type ErrorImporter struct{} + +// Import fetches data from a map entry. +// All paths are treated as absolute keys. +func (importer *ErrorImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) { + return jsonnet.Contents{}, "", fmt.Errorf("import not available %v", importedPath) +} + +func JsonnetTestBinary(t testing.TB) string { + t.Helper() + + // We can force the usage of a given jsonnet executable. + // Useful to test different versions, or run the tests under wine. + if s := os.Getenv("ORY_JSONNET_PATH"); s != "" { + return s + } + + var stderr bytes.Buffer + // Using `t.TempDir()` results in permissions errors on Windows, sometimes. + outPath := path.Join(os.TempDir(), "jsonnet") + if runtime.GOOS == "windows" { + outPath = outPath + ".exe" + } + cmd := exec.Command("go", "build", "-o", outPath, "github.com/ory/x/jsonnetsecure/cmd") + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil || stderr.Len() != 0 { + t.Fatalf("building the Go binary returned error: %v\n%s", err, stderr.String()) + } + + return outPath +} diff --git a/oryx/jsonnetsecure/jsonnet_pool.go b/oryx/jsonnetsecure/jsonnet_pool.go new file mode 100644 index 0000000..2732d1e --- /dev/null +++ b/oryx/jsonnetsecure/jsonnet_pool.go @@ -0,0 +1,313 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +// Known limitations/edge cases: +// - The child process exiting early (e.g. crashing) or getting killed (e.g. reaching some OS limit) +// is not detected and no error will be returned in this case from `eval()`. +// - Misbehaving jsonnet scripts in the middle of a batch being passed to the child process for evaluation may result in +// no error (as mentioned above), and other valid scripts in this batch may result +// in an error (because the output from the child process is truncated). +// +// Possible remediations: +// - Do not pass a batch of scripts to a worker, only pass one script at a time (to isolate misbehaving scripts) +// - Validate that the output is valid JSON (to detect truncated output) +// - Detect the child process exiting (to return an error) + +import ( + "bufio" + "context" + "encoding/json" + "io" + "math" + "os/exec" + "strings" + "time" + + "github.com/jackc/puddle/v2" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" +) + +const ( + KiB = 1024 + jsonnetOutputLimit = 256 * KiB + jsonnetErrLimit = 1 * KiB +) + +type ( + processPoolVM struct { + path string + args []string + ctx context.Context + params processParameters + pool *pool + } + Pool interface { + Close() + // Stat returns a snapshot of the worker pool's statistics. + Stat() *puddle.Stat + private() + } + pool struct { + puddle *puddle.Pool[worker] + } + worker struct { + cmd *exec.Cmd + stdin chan<- []byte + stdout <-chan string + stderr <-chan string + } + contextKeyType string +) + +var ( + ErrProcessPoolClosed = errors.New("jsonnetsecure: process pool closed") + + _ VM = (*processPoolVM)(nil) + _ Pool = (*pool)(nil) + + contextValuePath contextKeyType = "argc" + contextValueArgs contextKeyType = "argv" +) + +func NewProcessPool(size int) Pool { + size = max(5, min(size, math.MaxInt32)) + pud, err := puddle.NewPool(&puddle.Config[worker]{ + MaxSize: int32(size), //nolint:gosec // disable G115 // because of the previous min/max, 5 <= size <= math.MaxInt32 + Constructor: newWorker, + Destructor: worker.destroy, + }) + if err != nil { + panic(err) // this should never happen, see implementation of puddle.NewPool + } + for range size { + // warm pool + go pud.CreateResource(context.Background()) + } + go func() { + for { + time.Sleep(10 * time.Second) + for _, proc := range pud.AcquireAllIdle() { + if proc.Value().cmd.ProcessState != nil { + proc.Destroy() + } else { + proc.Release() + } + } + } + }() + return &pool{pud} +} + +func (*pool) private() {} + +func (p *pool) Close() { + p.puddle.Close() +} + +func (p *pool) Stat() *puddle.Stat { + return p.puddle.Stat() +} + +func newWorker(ctx context.Context) (_ worker, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jsonnetsecure.newWorker") + defer otelx.End(span, &err) + + path, _ := ctx.Value(contextValuePath).(string) + if path == "" { + return worker{}, errors.New("newWorker: missing binary path in context") + } + args, _ := ctx.Value(contextValueArgs).([]string) + cmd := exec.Command(path, append(args, "-0")...) + cmd.Env = []string{"GOMAXPROCS=1"} + cmd.WaitDelay = 100 * time.Millisecond + + span.SetAttributes(semconv.ProcessCommand(cmd.Path), semconv.ProcessCommandArgs(cmd.Args...)) + + stdin, err := cmd.StdinPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stdin pipe") + } + + in := make(chan []byte, 1) + go func(c <-chan []byte) { + for input := range c { + if _, err := stdin.Write(append(input, 0)); err != nil { + stdin.Close() + return + } + } + }(in) + + stdout, err := cmd.StdoutPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stdout pipe") + } + stderr, err := cmd.StderrPipe() + if err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to create stderr pipe") + } + + if err := cmd.Start(); err != nil { + return worker{}, errors.Wrap(err, "newWorker: failed to start process") + } + + span.SetAttributes(semconv.ProcessPID(cmd.Process.Pid)) + + scan := func(c chan<- string, r io.Reader, maxTokenSize int) { + defer close(c) + scanner := bufio.NewScanner(r) + scanner.Buffer(make([]byte, 0, 64*KiB), maxTokenSize) + + scanner.Split(splitNull) + for scanner.Scan() { + c <- scanner.Text() + } + if err := scanner.Err(); err != nil { + c <- "ERROR: scan: " + err.Error() + } + } + out := make(chan string, 1) + go scan(out, stdout, jsonnetOutputLimit) + errs := make(chan string, 1) + go scan(errs, stderr, jsonnetErrLimit) + + w := worker{ + cmd: cmd, + stdin: in, + stdout: out, + stderr: errs, + } + _, err = w.eval(ctx, []byte("{}")) // warm up + if err != nil { + w.destroy() + return worker{}, errors.Wrap(err, "newWorker: warm up failed") + } + + return w, nil +} + +func (w worker) destroy() { + close(w.stdin) + w.cmd.Process.Kill() + w.cmd.Wait() +} + +func (w worker) eval(ctx context.Context, processParams []byte) (output string, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jsonnetsecure.worker.eval", trace.WithAttributes( + semconv.ProcessPID(w.cmd.Process.Pid))) + defer otelx.End(span, &err) + + // The worker is exclusively acquired, so the schedstat delta over this + // evaluation is attributable to it. cpu_time_us is time actually spent on + // a CPU; runqueue_wait_us is time runnable but waiting for one. Span wall + // time far exceeding their sum means the process was not even runnable, + // e.g. dequeued by cgroup CPU throttling. + // + // A kernel without CONFIG_SCHED_INFO reports "0 0 0" instead of failing + // the read. The worker has already spent CPU time on its warm-up + // evaluation, so a zero cpuTime means the fields are dead — skip them + // rather than record misleading zeros. + if before, ok := readSchedstat(w.cmd.Process.Pid); ok && before.cpuTime > 0 { + defer func() { + after, ok := readSchedstat(w.cmd.Process.Pid) + if !ok { + return + } + span.SetAttributes( + attribute.Int64("jsonnet.worker.cpu_time_us", (after.cpuTime-before.cpuTime).Microseconds()), + attribute.Int64("jsonnet.worker.runqueue_wait_us", (after.runqueueWait-before.runqueueWait).Microseconds()), + ) + }() + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case w.stdin <- processParams: + break + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case output := <-w.stdout: + return output, nil + case err := <-w.stderr: + return "", errors.New(err) + } +} + +func (vm *processPoolVM) EvaluateAnonymousSnippet(filename string, snippet string) (_ string, err error) { + tracer := trace.SpanFromContext(vm.ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(vm.ctx, "jsonnetsecure.processPoolVM.EvaluateAnonymousSnippet", trace.WithAttributes(attribute.String("filename", filename))) + defer otelx.End(span, &err) + + params := vm.params + params.Filename = filename + params.Snippet = snippet + pp, err := json.Marshal(params) + if err != nil { + return "", errors.Wrap(err, "jsonnetsecure: marshal") + } + + ctx = context.WithValue(ctx, contextValuePath, vm.path) + ctx = context.WithValue(ctx, contextValueArgs, vm.args) + worker, err := vm.pool.puddle.Acquire(ctx) + if err != nil { + return "", errors.Wrap(err, "jsonnetsecure: acquire") + } + + ctx, cancel := context.WithTimeoutCause(ctx, 1*time.Second, errors.Errorf("failed to run jsonnet within 1s: filename=%s", filename)) + defer cancel() + result, err := worker.Value().eval(ctx, pp) + if err != nil { + worker.Destroy() + return "", errors.Wrap(err, "jsonnetsecure: eval") + } else { + worker.Release() + } + + if strings.HasPrefix(result, "ERROR: ") { + return "", errors.New("jsonnetsecure: " + result) + } + + return result, nil +} + +func NewProcessPoolVM(opts *vmOptions) VM { + ctx := opts.ctx + if ctx == nil { + ctx = context.Background() + } + return &processPoolVM{ + path: opts.jsonnetBinaryPath, + args: opts.args, + ctx: ctx, + pool: opts.pool, + } +} + +func (vm *processPoolVM) ExtCode(key string, val string) { + vm.params.ExtCodes = append(vm.params.ExtCodes, kv{key, val}) +} + +func (vm *processPoolVM) ExtVar(key string, val string) { + vm.params.ExtVars = append(vm.params.ExtVars, kv{key, val}) +} + +func (vm *processPoolVM) TLACode(key string, val string) { + vm.params.TLACodes = append(vm.params.TLACodes, kv{key, val}) +} + +func (vm *processPoolVM) TLAVar(key string, val string) { + vm.params.TLAVars = append(vm.params.TLAVars, kv{key, val}) +} diff --git a/oryx/jsonnetsecure/limit_unix.go b/oryx/jsonnetsecure/limit_unix.go new file mode 100644 index 0000000..b02566d --- /dev/null +++ b/oryx/jsonnetsecure/limit_unix.go @@ -0,0 +1,29 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !windows + +package jsonnetsecure + +import ( + "fmt" + "runtime/debug" + "syscall" + + "github.com/pkg/errors" +) + +func SetVirtualMemoryLimit(limitBytes uint64) error { + // Tell the Go runtime about the limit. + debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant. + + lim := syscall.Rlimit{ + Cur: limitBytes, + Max: limitBytes, + } + err := syscall.Setrlimit(syscall.RLIMIT_AS, &lim) + if err != nil { + return errors.WithStack(fmt.Errorf("failed to set virtual memory limit: %v", err)) + } + return nil +} diff --git a/oryx/jsonnetsecure/limit_windows.go b/oryx/jsonnetsecure/limit_windows.go new file mode 100644 index 0000000..3557c7a --- /dev/null +++ b/oryx/jsonnetsecure/limit_windows.go @@ -0,0 +1,16 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows + +package jsonnetsecure + +import "runtime/debug" + +func SetVirtualMemoryLimit(limitBytes uint64) error { + // Tell the Go runtime about the limit. + debug.SetMemoryLimit(int64(limitBytes)) //nolint:gosec // The number is a compile-time constant. + + // TODO No OS limit for now. Apparently there is a Windows-specific equivalent (Job control)? + return nil +} diff --git a/oryx/jsonnetsecure/null.go b/oryx/jsonnetsecure/null.go new file mode 100644 index 0000000..42d6e92 --- /dev/null +++ b/oryx/jsonnetsecure/null.go @@ -0,0 +1,22 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import "bytes" + +func splitNull(data []byte, atEOF bool) (advance int, token []byte, err error) { + // Look for a null byte; if found, return the position after it, + // the data before it, and no error. + if i := bytes.IndexByte(data, 0); i >= 0 { + return i + 1, data[0:i], nil + } + + // If we're at EOF, we have a final, non-terminated word. Return it. + if atEOF && len(data) != 0 { + return len(data), data, nil + } + + // Request more data. + return 0, nil, nil +} diff --git a/oryx/jsonnetsecure/provider.go b/oryx/jsonnetsecure/provider.go new file mode 100644 index 0000000..2407109 --- /dev/null +++ b/oryx/jsonnetsecure/provider.go @@ -0,0 +1,60 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "context" + "os" + "runtime" + "testing" +) + +type ( + VMProvider interface { + // JsonnetVM creates a new secure process-isolated Jsonnet VM whose + // execution is bound to the provided context, i.e., + // cancelling the context will terminate the VM process. + JsonnetVM(context.Context) (VM, error) + } + + // TestProvider provides a secure VM by running go build on github.com/ory/x/jsonnetsecure/cmd + TestProvider struct { + jsonnetBinary string + pool Pool + } + + // DefaultProvider provides a secure VM by calling the currently + // running the current binary with the provided subcommand. + DefaultProvider struct { + Subcommand string + Pool Pool + } +) + +func NewTestProvider(t testing.TB) *TestProvider { + pool := NewProcessPool(runtime.GOMAXPROCS(0)) + t.Cleanup(pool.Close) + return &TestProvider{JsonnetTestBinary(t), pool} +} + +func (p *TestProvider) JsonnetVM(ctx context.Context) (VM, error) { + return MakeSecureVM( + p.pool, + WithContext(ctx), + WithJsonnetBinary(p.jsonnetBinary), + ) +} + +func (p *DefaultProvider) JsonnetVM(ctx context.Context) (VM, error) { + self, err := os.Executable() + if err != nil { + return nil, err + } + return MakeSecureVM( + p.Pool, + WithContext(ctx), + WithJsonnetBinary(self), + WithProcessArgs(p.Subcommand), + ) +} diff --git a/oryx/jsonnetsecure/schedstat.go b/oryx/jsonnetsecure/schedstat.go new file mode 100644 index 0000000..9829b7c --- /dev/null +++ b/oryx/jsonnetsecure/schedstat.go @@ -0,0 +1,53 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetsecure + +import ( + "bytes" + "os" + "strconv" + "time" +) + +// schedstat holds the scheduler statistics of a process as reported by the +// Linux kernel in /proc//schedstat: the CPU time the process actually +// spent running, and the time it spent runnable but waiting for a CPU (due to +// contention or cgroup CPU throttling). +type schedstat struct { + cpuTime time.Duration + runqueueWait time.Duration +} + +// readSchedstat returns the cumulative scheduler statistics for pid. It +// reports ok=false on platforms without /proc//schedstat (e.g. macOS, +// Windows) and on Linux kernels built without CONFIG_SCHED_INFO. +func readSchedstat(pid int) (schedstat, bool) { + raw, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/schedstat") + if err != nil { + return schedstat{}, false + } + return parseSchedstat(raw) +} + +// parseSchedstat parses the /proc//schedstat format: three +// space-separated integers (run time in nanoseconds, runqueue wait time in +// nanoseconds, number of timeslices). +func parseSchedstat(raw []byte) (schedstat, bool) { + fields := bytes.Fields(raw) + if len(fields) < 2 { + return schedstat{}, false + } + cpuTime, err := strconv.ParseInt(string(fields[0]), 10, 64) + if err != nil || cpuTime < 0 { + return schedstat{}, false + } + runqueueWait, err := strconv.ParseInt(string(fields[1]), 10, 64) + if err != nil || runqueueWait < 0 { + return schedstat{}, false + } + return schedstat{ + cpuTime: time.Duration(cpuTime), + runqueueWait: time.Duration(runqueueWait), + }, true +} diff --git a/oryx/jsonnetsecure/stub/import.jsonnet b/oryx/jsonnetsecure/stub/import.jsonnet new file mode 100644 index 0000000..02b09a7 --- /dev/null +++ b/oryx/jsonnetsecure/stub/import.jsonnet @@ -0,0 +1 @@ +{ foo: 'bar' } diff --git a/oryx/jsonnetx/format.go b/oryx/jsonnetx/format.go new file mode 100644 index 0000000..767886b --- /dev/null +++ b/oryx/jsonnetx/format.go @@ -0,0 +1,76 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "fmt" + "os" + + "github.com/bmatcuk/doublestar/v2" + "github.com/google/go-jsonnet/formatter" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewFormatCommand() *cobra.Command { + var verbose, write bool + cmd := &cobra.Command{ + Use: "format path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]", + Long: `Formats JSONNet files using the official JSONNet formatter. + +Use -w or --write to write output back to files instead of stdout. + +` + GlobHelp, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + for _, pattern := range args { + files, err := doublestar.Glob(pattern) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Glob pattern %q is not valid: %s\n", pattern, err) + return cmdx.FailSilently(cmd) + } + + for _, file := range files { + if fi, err := os.Stat(file); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Matching file %q could not be opened: %s\n", file, err) + return cmdx.FailSilently(cmd) + } else if fi.IsDir() { + continue + } + + if verbose { + fmt.Printf("Processing file: %s\n", file) + } + + //#nosec G304 -- false positive + content, err := os.ReadFile(file) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to read file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + + output, err := formatter.Format(file, string(content), formatter.DefaultOptions()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "File %q could not be formatted: %s", file, err) + } + + if write { + err := os.WriteFile(file, []byte(output), 0o644) // #nosec + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to write file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + } else { + fmt.Println(output) + } + } + } + return nil + }, + } + cmd.Flags().BoolVarP(&write, "write", "w", false, "Write formatted output back to file.") + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output.") + return cmd +} diff --git a/oryx/jsonnetx/lint.go b/oryx/jsonnetx/lint.go new file mode 100644 index 0000000..bbba28a --- /dev/null +++ b/oryx/jsonnetx/lint.go @@ -0,0 +1,63 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "fmt" + "os" + + "github.com/bmatcuk/doublestar/v2" + "github.com/google/go-jsonnet" + "github.com/google/go-jsonnet/linter" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" +) + +func NewLintCommand() *cobra.Command { + var verbose bool + cmd := &cobra.Command{ + Use: "lint path/to/files/*.jsonnet [more/files.jsonnet, [supports/**/{foo,bar}.jsonnet]]", + Long: `Lints JSONNet files using the official JSONNet linter and exits with a status code of 1 when issues are detected. + +` + GlobHelp, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + for _, pattern := range args { + files, err := doublestar.Glob(pattern) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Glob pattern %q is not valid: %s\n", pattern, err) + return cmdx.FailSilently(cmd) + } + + for _, file := range files { + if fi, err := os.Stat(file); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Matching file %q could not be opened: %s\n", file, err) + return cmdx.FailSilently(cmd) + } else if fi.IsDir() { + continue + } + + if verbose { + fmt.Printf("Processing file: %s\n", file) + } + + //#nosec G304 -- false positive + content, err := os.ReadFile(file) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Unable to read file %q: %s\n", file, err) + return cmdx.FailSilently(cmd) + } + + if linter.LintSnippet(jsonnet.MakeVM(), cmd.ErrOrStderr(), []linter.Snippet{{FileName: file, Code: string(content)}}) { + return cmdx.FailSilently(cmd) + } + } + } + return nil + }, + } + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Verbose output.") + return cmd +} diff --git a/oryx/jsonnetx/root.go b/oryx/jsonnetx/root.go new file mode 100644 index 0000000..01644d5 --- /dev/null +++ b/oryx/jsonnetx/root.go @@ -0,0 +1,56 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonnetx + +import ( + "github.com/spf13/cobra" +) + +const GlobHelp = `Glob patterns supports the following special terms in the patterns: + + Special Terms | Meaning + ------------- | ------- + '*' | matches any sequence of non-path-separators + '**' | matches any sequence of characters, including path separators + '?' | matches any single non-path-separator character + '[class]' | matches any single non-path-separator character against a class of characters ([see below](#character-classes)) + '{alt1,...}' | matches a sequence of characters if one of the comma-separated alternatives matches + + Any character with a special meaning can be escaped with a backslash ('\'). + + #### Character Classes + + Character classes support the following: + + Class | Meaning + ---------- | ------- + '[abc]' | matches any single character within the set + '[a-z]' | matches any single character in the range + '[^class]' | matches any single character which does *not* match the class +` + +// RootCommand represents the jsonnet command +// Deprecated: use NewRootCommand instead. +var RootCommand = &cobra.Command{ + Use: "jsonnet", + Short: "Helpers for linting and formatting JSONNet code", +} + +// RegisterCommandRecursive adds all jsonnet helpers to the RootCommand +// Deprecated: use NewRootCommand instead. +func RegisterCommandRecursive(parent *cobra.Command) { + parent.AddCommand(RootCommand) + + RootCommand.AddCommand(NewFormatCommand()) + RootCommand.AddCommand(NewLintCommand()) +} + +func NewRootCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "jsonnet", + Short: "Helpers for linting and formatting JSONNet code", + } + cmd.AddCommand(NewFormatCommand(), NewLintCommand()) + return cmd +} diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json new file mode 100644 index 0000000..f9ee1c4 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=0.json @@ -0,0 +1,3534 @@ +[ + { + "Title": "Access Rules", + "Description": "Configure access rules. All sub-keys support configuration reloading without restarting.", + "Examples": null, + "Name": "access_rules", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Repositories", + "Description": "Locations (list of URLs) where access rules should be fetched from on boot. It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules:\n\n- If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be fetched from the local file system.\n- If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{\"id\":\"foo-rule\",\"authenticators\":[....]}]`)).\n- If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be fetched from the provided HTTP(s) location.", + "Examples": [ + "[\"file://path/to/rules.json\",\"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d\",\"https://path-to-my-rules/rules.json\"]" + ], + "Name": "access_rules.repositories", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "access_rules.repositories.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Authenticators", + "Description": "For more information on authenticators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authn", + "Examples": null, + "Name": "authenticators", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous", + "Description": "The [`anonymous` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#anonymous).", + "Examples": null, + "Name": "authenticators.anonymous", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.anonymous.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Anonymous Subject", + "Description": "Sets the anonymous username.", + "Examples": [ + "guest", + "anon", + "anonymous", + "unknown" + ], + "Name": "authenticators.anonymous.config.subject", + "Default": "anonymous", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.anonymous.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Session", + "Description": "The [`cookie_session` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#cookie_session).", + "Examples": null, + "Name": "authenticators.cookie_session", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Session Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.cookie_session.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Session Check URL", + "Description": "The origin to proxy requests to. If the response is a 200 with body `{ \"subject\": \"...\", \"extra\": {} }`. The request will pass the subject through successfully, otherwise it will be marked as unauthorized.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://session-store-host" + ], + "Name": "authenticators.cookie_session.config.check_session_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Only Cookies", + "Description": "A list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if none of the passed cookies are set on the request.", + "Examples": null, + "Name": "authenticators.cookie_session.config.only", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.cookie_session.config.only.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.cookie_session.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Token (jwt)", + "Description": "The [`jwt` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#jwt).", + "Examples": null, + "Name": "authenticators.jwt", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JWT Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.jwt.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.allowed_algorithms", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.allowed_algorithms.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Key URLs", + "Description": "URLs where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web Token. Usually something like \"https://my-keys.com/.well-known/jwks.json\". The response of that endpoint must return a JSON Web Key Set (JWKS).\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ], + "Name": "authenticators.jwt.config.jwks_urls", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.jwks_urls.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Required Token Scope", + "Description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.jwt.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Scope Strategy", + "Description": "Sets the strategy validation algorithm.", + "Examples": null, + "Name": "authenticators.jwt.config.scope_strategy", + "Default": "none", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "hierarchic", + "exact", + "wildcard", + "none" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Intended Audience", + "Description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "Examples": null, + "Name": "authenticators.jwt.config.target_audience", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.target_audience.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.token_from", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header", + "Description": "The header (case insensitive) that must contain a token for request authentication. It can't be set along with query_parameter.", + "Examples": null, + "Name": "authenticators.jwt.config.token_from.header", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Query Parameter", + "Description": "The query parameter (case sensitive) that must contain a token for request authentication. It can't be set along with header.", + "Examples": null, + "Name": "authenticators.jwt.config.token_from.query_parameter", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.trusted_issuers", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.jwt.config.trusted_issuers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.jwt.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "No Operation (noop)", + "Description": "The [`noop` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#noop).", + "Examples": null, + "Name": "authenticators.noop", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.noop.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Credentials", + "Description": "The [`oauth2_client_credentials` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_client_credentials).", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Credentials Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Request Permissions (Token Scope)", + "Description": "Scopes is an array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this rule.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_client_credentials.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "The OAuth 2.0 Token Endpoint that will be used to validate the client credentials.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/oauth2/token" + ], + "Name": "authenticators.oauth2_client_credentials.config.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.oauth2_client_credentials.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Token Introspection", + "Description": "The [`oauth2_introspection` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_introspection).", + "Examples": null, + "Name": "authenticators.oauth2_introspection", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Introspection Authenticator Configuration", + "Description": "This section is optional when the authenticator is disabled.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Introspection URL", + "Description": "The OAuth 2.0 Token Introspection endpoint URL.\n\n\u003eIf this authenticator is enabled, this value is required.", + "Examples": [ + "https://my-website.com/oauth2/introspection" + ], + "Name": "authenticators.oauth2_introspection.config.introspection_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Pre-Authorization", + "Description": "Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client ID", + "Description": "The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.client_id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Client Secret", + "Description": "The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.client_secret", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.enabled", + "Default": null, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": [ + true + ], + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Scope", + "Description": "The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant.", + "Examples": [ + [ + "[\"foo\", \"bar\"]" + ] + ], + "Name": "authenticators.oauth2_introspection.config.pre_authorization.scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "OAuth 2.0 Token URL", + "Description": "The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed.\n\n\u003eIf pre-authorization is enabled, this value is required.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.pre_authorization.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Required Scope", + "Description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.required_scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.required_scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Scope Strategy", + "Description": "Sets the strategy validation algorithm.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.scope_strategy", + "Default": "none", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "hierarchic", + "exact", + "wildcard", + "none" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Target Audience", + "Description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.target_audience", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.target_audience.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Token From", + "Description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header", + "Description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from.header", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Query Parameter", + "Description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.token_from.query_parameter", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Trusted Issuers", + "Description": "The token must have been issued by one of the issuers listed in this array.", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.trusted_issuers", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authenticators.oauth2_introspection.config.trusted_issuers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.oauth2_introspection.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Unauthorized", + "Description": "The [`unauthorized` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#unauthorized).", + "Examples": null, + "Name": "authenticators.unauthorized", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authenticators.unauthorized.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Authorizers", + "Description": "For more information on authorizers head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authz", + "Examples": null, + "Name": "authorizers", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow", + "Description": "The [`allow` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "Examples": null, + "Name": "authorizers.allow", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.allow.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Deny", + "Description": "The [`deny` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "Examples": null, + "Name": "authorizers.deny", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.deny.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ORY Keto Access Control Policies Engine", + "Description": "The [`keto_engine_acp_ory` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#keto_engine_acp_ory).", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ORY Keto Access Control Policy Authorizer Configuration", + "Description": "This section is optional when the authorizer is disabled.", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base URL", + "Description": "The base URL of ORY Keto.\n\n\u003eIf this authorizer is enabled, this value is required.", + "Examples": [ + "http://my-keto/" + ], + "Name": "authorizers.keto_engine_acp_ory.config.base_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.flavor", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.required_action", + "Default": "unset", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.required_resource", + "Default": "unset", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "authorizers.keto_engine_acp_ory.config.subject", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "authorizers.keto_engine_acp_ory.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Log", + "Description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "Examples": null, + "Name": "log", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Format", + "Description": "The log format can either be text or JSON.", + "Examples": null, + "Name": "log.format", + "Default": "text", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "text", + "json" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Level", + "Description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL.", + "Examples": null, + "Name": "log.level", + "Default": "info", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "panic", + "fatal", + "error", + "warn", + "info", + "debug" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Mutators", + "Description": "For more information on mutators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/mutator", + "Examples": null, + "Name": "mutators", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Cookie", + "Description": "The [`cookie` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#cookie).", + "Examples": null, + "Name": "mutators.cookie", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cookie Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.cookie.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.cookie.config.cookies", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.cookie.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Header", + "Description": "The [`header` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#header).", + "Examples": null, + "Name": "mutators.header", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Header Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.header.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.header.config.headers", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.header.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Hydrator", + "Description": "The [`hydrator` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#hydrator).", + "Examples": null, + "Name": "mutators.hydrator", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Hydrator Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.hydrator.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic.password", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.auth.basic.username", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry.delay_in_milliseconds", + "Default": 3, + "Type": 0, + "TypeHint": 3, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": "0", + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.retry.number_of_retries", + "Default": 100, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": "0", + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.hydrator.config.api.url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.hydrator.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ID Token (JSON Web Token)", + "Description": "The [`id_token` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#id_token).", + "Examples": null, + "Name": "mutators.id_token", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "ID Token Mutator Configuration", + "Description": "This section is optional when the mutator is disabled.", + "Examples": null, + "Name": "mutators.id_token.config", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "mutators.id_token.config.claims", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Issuer URL", + "Description": "Sets the \"iss\" value of the ID Token.\n\n\u003eIf this mutator is enabled, this value is required.", + "Examples": null, + "Name": "mutators.id_token.config.issuer_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "JSON Web Key URL", + "Description": "Sets the URL where keys should be fetched from. Supports remote locations (http, https) as well as local filesystem paths.\n\n\u003eIf this mutator is enabled, this value is required.", + "Examples": [ + "https://fetch-keys/from/this/location.json", + "file:///from/this/absolute/location.json", + "file://../from/this/relative/location.json" + ], + "Name": "mutators.id_token.config.jwks_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Expire After", + "Description": "Sets the time-to-live of the JSON Web Token.", + "Examples": [ + "1h", + "1m", + "30s" + ], + "Name": "mutators.id_token.config.ttl", + "Default": "1m", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.id_token.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "No Operation (noop)", + "Description": "The [`noop` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#noop).", + "Examples": null, + "Name": "mutators.noop", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enabled", + "Description": "En-/disables this component.", + "Examples": [ + true + ], + "Name": "mutators.noop.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Profiling", + "Description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "Examples": null, + "Name": "profiling", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "cpu", + "mem" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP(s)", + "Description": "", + "Examples": null, + "Name": "serve", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP REST API", + "Description": "", + "Examples": null, + "Name": "serve.api", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cross Origin Resource Sharing (CORS)", + "Description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "Examples": null, + "Name": "serve.api.cors", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow HTTP Credentials", + "Description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "Examples": null, + "Name": "serve.api.cors.allow_credentials", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Request HTTP Headers", + "Description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.api.cors.allowed_headers", + "Default": [ + "Authorization", + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed HTTP Methods", + "Description": "A list of methods the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.api.cors.allowed_methods", + "Default": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_methods.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Origins", + "Description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "Examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ], + "Name": "serve.api.cors.allowed_origins", + "Default": [ + "*" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.allowed_origins.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable Debugging", + "Description": "Set to true to debug server side CORS issues.", + "Examples": null, + "Name": "serve.api.cors.debug", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable CORS", + "Description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered.", + "Examples": null, + "Name": "serve.api.cors.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Response HTTP Headers", + "Description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "Examples": null, + "Name": "serve.api.cors.exposed_headers", + "Default": [ + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.cors.exposed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Maximum Age", + "Description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age.", + "Examples": null, + "Name": "serve.api.cors.max_age", + "Default": 0, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Host", + "Description": "The network interface to listen on.", + "Examples": [ + "localhost", + "127.0.0.1" + ], + "Name": "serve.api.host", + "Default": "", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Port", + "Description": "The port to listen on.", + "Examples": null, + "Name": "serve.api.port", + "Default": 4456, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTPS", + "Description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "Examples": null, + "Name": "serve.api.tls", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.tls.cert", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.api.tls.cert.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.api.tls.cert.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.api.tls.key", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.api.tls.key.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.api.tls.key.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Reverse Proxy", + "Description": "", + "Examples": null, + "Name": "serve.proxy", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Cross Origin Resource Sharing (CORS)", + "Description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "Examples": null, + "Name": "serve.proxy.cors", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allow HTTP Credentials", + "Description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.", + "Examples": null, + "Name": "serve.proxy.cors.allow_credentials", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Request HTTP Headers", + "Description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.proxy.cors.allowed_headers", + "Default": [ + "Authorization", + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed HTTP Methods", + "Description": "A list of methods the client is allowed to use with cross-domain requests.", + "Examples": null, + "Name": "serve.proxy.cors.allowed_methods", + "Default": [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_methods.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Origins", + "Description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "Examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ], + "Name": "serve.proxy.cors.allowed_origins", + "Default": [ + "*" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.allowed_origins.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable Debugging", + "Description": "Set to true to debug server side CORS issues.", + "Examples": null, + "Name": "serve.proxy.cors.debug", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Enable CORS", + "Description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered.", + "Examples": null, + "Name": "serve.proxy.cors.enabled", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Allowed Response HTTP Headers", + "Description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "Examples": null, + "Name": "serve.proxy.cors.exposed_headers", + "Default": [ + "Content-Type" + ], + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.cors.exposed_headers.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Maximum Age", + "Description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age.", + "Examples": null, + "Name": "serve.proxy.cors.max_age", + "Default": 0, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Host", + "Description": "The network interface to listen on. Leave empty to listen on all interfaces.", + "Examples": [ + "localhost", + "127.0.0.1" + ], + "Name": "serve.proxy.host", + "Default": "", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Port", + "Description": "The port to listen on.", + "Examples": null, + "Name": "serve.proxy.port", + "Default": 4455, + "Type": 0, + "TypeHint": 2, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Timeouts", + "Description": "Control the reverse proxy's HTTP timeouts.", + "Examples": null, + "Name": "serve.proxy.timeout", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Idle Timeout", + "Description": " The maximum amount of time to wait for any action of a request session, reading data or writing the response.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.idle", + "Default": "120s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Read Timeout", + "Description": "The maximum duration for reading the entire request, including the body.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.read", + "Default": "5s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTP Write Timeout", + "Description": "The maximum duration before timing out writes of the response. Increase this parameter to prevent unexpected closing a client connection if an upstream request is responding slowly.", + "Examples": [ + "5s", + "5m", + "5h" + ], + "Name": "serve.proxy.timeout.write", + "Default": "120s", + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "HTTPS", + "Description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "Examples": null, + "Name": "serve.proxy.tls", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.tls.cert", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.proxy.tls.cert.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.proxy.tls.cert.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "serve.proxy.tls.key", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Base64 Encoded Inline", + "Description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "Examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ], + "Name": "serve.proxy.tls.key.base64", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Path to PEM-encoded Fle", + "Description": "", + "Examples": [ + "path/to/file.pem" + ], + "Name": "serve.proxy.tls.key.path", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json new file mode 100644 index 0000000..f1b5721 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=1.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json new file mode 100644 index 0000000..cf25af4 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=2.json @@ -0,0 +1,23 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "dsn", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json new file mode 100644 index 0000000..88d7567 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=3.json @@ -0,0 +1,305 @@ +[ + { + "Title": "OpenID Connect and OAuth2 Providers", + "Description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://accounts.google.com/o/oauth2/v2/auth" + ], + "Name": "providers.#.auth_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.client_id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.client_secret", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "google" + ], + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://accounts.google.com" + ], + "Name": "providers.#.issuer_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Jsonnet Mapper URL", + "Description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "Examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ], + "Name": "providers.#.mapper_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Provider", + "Description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "Examples": [ + "google" + ], + "Name": "providers.#.provider", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.scope", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "offline_access", + "profile" + ], + "Name": "providers.#.scope.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "Azure AD Tenant", + "Description": "The Azure AD Tenant to use for authentication.", + "Examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ], + "Name": "providers.#.tenant", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": [ + "https://www.googleapis.com/oauth2/v4/token" + ], + "Name": "providers.#.token_url", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "uri", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json new file mode 100644 index 0000000..6513d10 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=4.json @@ -0,0 +1,86 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": "asdf", + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": true, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json new file mode 100644 index 0000000..6513d10 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=5.json @@ -0,0 +1,86 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": "asdf", + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": true, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": false, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "list.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json new file mode 100644 index 0000000..52c81f2 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=6.json @@ -0,0 +1,46 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": { + "foobar": "bar" + } + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "foo", + "Default": null, + "Type": false, + "TypeHint": 4, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json new file mode 100644 index 0000000..7deafa7 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=7.json @@ -0,0 +1,44 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json new file mode 100644 index 0000000..4dbf7ff --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=8.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#", + "Default": null, + "Type": [], + "TypeHint": 8, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#.#", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json b/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json new file mode 100644 index 0000000..78536e1 --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPaths-case=9.json @@ -0,0 +1,65 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "baz.#.foo", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json b/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json new file mode 100644 index 0000000..3e460cb --- /dev/null +++ b/oryx/jsonschemax/.snapshots/TestListPathsWithRecursion-case=0.json @@ -0,0 +1,233 @@ +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar.foo", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foo.bars", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "email", + "Pattern": ".*", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foo.bars", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "email", + "Pattern": ".*", + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": true, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "bar.foos", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": 1, + "MaxLength": 10, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] diff --git a/oryx/jsonschemax/README.md b/oryx/jsonschemax/README.md new file mode 100644 index 0000000..5306c69 --- /dev/null +++ b/oryx/jsonschemax/README.md @@ -0,0 +1,120 @@ +# JSON Schema Helpers + +This package contains utilities for working with JSON Schemas. + +## Listing all Possible JSON Schema Paths + +Using `jsonschemax.ListPaths()` you can get a list of all possible JSON paths in +a JSON Schema. + +```go +package main + +import ( + "bytes" + "fmt" + "github.com/ory/jsonschema/v3" + "github.com/ory/x/jsonschemax" +) + +var schema = "..." + +func main() { + c := jsonschema.NewCompiler() + _ = c.AddResource("test.json", bytes.NewBufferString(schema)) + paths, _ := jsonschemax.ListPaths("test.json", c) + fmt.Printf("%+v", paths) +} +``` + +All keys are delimited using `.`. Please note that arrays are denoted with `#` +when `ListPathsWithArraysIncluded` is used. For example, the JSON Schema + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } +} +``` + +Results in paths: + +```json +[ + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers", + "Default": null, + "Type": [], + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#", + "Default": null, + "Type": {}, + "TypeHint": 5, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + }, + { + "Title": "", + "Description": "", + "Examples": null, + "Name": "providers.#.id", + "Default": null, + "Type": "", + "TypeHint": 1, + "Format": "", + "Pattern": null, + "Enum": null, + "Constant": null, + "ReadOnly": false, + "MinLength": -1, + "MaxLength": -1, + "Required": false, + "Minimum": null, + "Maximum": null, + "MultipleOf": null, + "CustomProperties": null + } +] +``` diff --git a/oryx/jsonschemax/error.go b/oryx/jsonschemax/error.go new file mode 100644 index 0000000..f8b0482 --- /dev/null +++ b/oryx/jsonschemax/error.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "github.com/ory/jsonschema/v3" +) + +// ErrorType is the schema error type. +type ErrorType int + +const ( + // ErrorTypeMissing represents a validation that failed because a value is missing. + ErrorTypeMissing ErrorType = iota + 1 +) + +// Error represents a schema error. +type Error struct { + // Type is the error type. + Type ErrorType + + // DocumentPointer is the JSON Pointer in the document. + DocumentPointer string + + // SchemaPointer is the JSON Pointer in the schema. + SchemaPointer string + + // DocumentFieldName is a pointer to the document in dot-notation: fo.bar.baz + DocumentFieldName string +} + +// NewFromSanthoshError converts github.com/santhosh-tekuri/jsonschema.ValidationError to Error. +func NewFromSanthoshError(validationError jsonschema.ValidationError) *Error { + return &Error{ + // DocumentPointer: JSONPointerToDotNotation(validationError.InstancePtr), + // SchemaPointer: JSONPointerToDotNotation(validationError.SchemaPtr), + // DocumentFieldName: JSONPointerToDotNotation(validationError.InstancePtr), + } +} diff --git a/oryx/jsonschemax/keys.go b/oryx/jsonschemax/keys.go new file mode 100644 index 0000000..ab9638c --- /dev/null +++ b/oryx/jsonschemax/keys.go @@ -0,0 +1,447 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "math/big" + "regexp" + "slices" + "sort" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/jsonschema/v3" +) + +type ( + byName []Path + PathEnhancer interface { + EnhancePath(Path) map[string]interface{} + } + TypeHint int +) + +func (s byName) Len() int { return len(s) } +func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byName) Less(i, j int) bool { return s[i].Name < s[j].Name } + +const ( + String TypeHint = iota + 1 + Float + Int + Bool + JSON + Nil + + BoolSlice + StringSlice + IntSlice + FloatSlice +) + +// Path represents a JSON Schema Path. +type Path struct { + // Title of the path. + Title string + + // Description of the path. + Description string + + // Examples of the path. + Examples []interface{} + + // Name is the JSON path name. + Name string + + // Default is the default value of that path. + Default interface{} + + // Type is a prototype (e.g. float64(0)) of the path type. + Type interface{} + + TypeHint + + // Format is the format of the path if defined + Format string + + // Pattern is the pattern of the path if defined + Pattern *regexp.Regexp + + // Enum are the allowed enum values + Enum []interface{} + + // first element in slice is constant value. note: slice is used to capture nil constant. + Constant []interface{} + + // ReadOnly is whether the value is readonly + ReadOnly bool + + // -1 if not specified + MinLength int + MaxLength int + + // Required if set indicates this field is required. + Required bool + + Minimum *big.Float + Maximum *big.Float + + MultipleOf *big.Float + + CustomProperties map[string]interface{} +} + +// ListPathsBytes works like ListPathsWithRecursion but prepares the JSON Schema itself. +func ListPathsBytes(ctx context.Context, raw json.RawMessage, maxRecursion int16) ([]Path, error) { + compiler := jsonschema.NewCompiler() + compiler.ExtractAnnotations = true + id := fmt.Sprintf("%x.json", sha256.Sum256(raw)) + if err := compiler.AddResource(id, bytes.NewReader(raw)); err != nil { + return nil, err + } + compiler.ExtractAnnotations = true + return runPathsFromCompiler(ctx, id, compiler, maxRecursion, false) +} + +// ListPathsWithRecursion will follow circular references until maxRecursion is reached, without +// returning an error. +func ListPathsWithRecursion(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion uint8) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, int16(maxRecursion), false) +} + +// ListPaths lists all paths of a JSON Schema. Will return an error +// if circular references are found. +func ListPaths(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, -1, false) +} + +// ListPathsWithArraysIncluded lists all paths of a JSON Schema. Will return an error +// if circular references are found. +// Includes arrays with `#`. +func ListPathsWithArraysIncluded(ctx context.Context, ref string, compiler *jsonschema.Compiler) ([]Path, error) { + return runPathsFromCompiler(ctx, ref, compiler, -1, true) +} + +// ListPathsWithInitializedSchema loads the paths from the schema without compiling it. +// +// You MUST ensure that the compiler was using `ExtractAnnotations = true`. +func ListPathsWithInitializedSchema(schema *jsonschema.Schema) ([]Path, error) { + return runPaths(schema, -1, false) +} + +// ListPathsWithInitializedSchemaAndArraysIncluded loads the paths from the schema without compiling it. +// +// You MUST ensure that the compiler was using `ExtractAnnotations = true`. +// Includes arrays with `#`. +func ListPathsWithInitializedSchemaAndArraysIncluded(schema *jsonschema.Schema) ([]Path, error) { + return runPaths(schema, -1, true) +} + +func runPathsFromCompiler(ctx context.Context, ref string, compiler *jsonschema.Compiler, maxRecursion int16, includeArrays bool) ([]Path, error) { + if compiler == nil { + compiler = jsonschema.NewCompiler() + } + + compiler.ExtractAnnotations = true + + schema, err := compiler.Compile(ctx, ref) + if err != nil { + return nil, errors.WithStack(err) + } + + return runPaths(schema, maxRecursion, includeArrays) +} + +func runPaths(schema *jsonschema.Schema, maxRecursion int16, includeArrays bool) ([]Path, error) { + pointers := map[string]bool{} + paths, err := listPaths(schema, nil, nil, pointers, 0, maxRecursion, includeArrays) + if err != nil { + return nil, errors.WithStack(err) + } + + sort.Stable(paths) + return makeUnique(paths) +} + +func makeUnique(in byName) (byName, error) { + cache := make(map[string]Path) + for _, p := range in { + vc, ok := cache[p.Name] + if !ok { + cache[p.Name] = p + continue + } + + if fmt.Sprintf("%T", p.Type) != fmt.Sprintf("%T", p.Type) { + return nil, errors.Errorf("multiple types %+v are not supported for path: %s", []interface{}{p.Type, vc.Type}, p.Name) + } + + if vc.Default == nil { + cache[p.Name] = p + } + } + + k := 0 + out := make([]Path, len(cache)) + for _, v := range cache { + out[k] = v + k++ + } + + paths := byName(out) + sort.Sort(paths) + return paths, nil +} + +func appendPointer(in map[string]bool, pointer *jsonschema.Schema) map[string]bool { + out := make(map[string]bool) + for k, v := range in { + out[k] = v + } + out[fmt.Sprintf("%p", pointer)] = true + return out +} + +func listPaths(schema *jsonschema.Schema, parent *jsonschema.Schema, parents []string, pointers map[string]bool, currentRecursion int16, maxRecursion int16, includeArrays bool) (byName, error) { + var pathType interface{} + var pathTypeHint TypeHint + var paths []Path + _, isCircular := pointers[fmt.Sprintf("%p", schema)] + + if len(schema.Constant) > 0 { + switch schema.Constant[0].(type) { + case float64, json.Number: + pathType = float64(0) + pathTypeHint = Float + case int8, int16, int, int64: + pathType = int64(0) + pathTypeHint = Int + case string: + pathType = "" + pathTypeHint = String + case bool: + pathType = false + pathTypeHint = Bool + default: + pathType = schema.Constant[0] + pathTypeHint = JSON + } + } else if len(schema.Types) == 1 { + switch schema.Types[0] { + case "null": + pathType = nil + pathTypeHint = Nil + case "boolean": + pathType = false + pathTypeHint = Bool + case "number": + pathType = float64(0) + pathTypeHint = Float + case "integer": + pathType = float64(0) + pathTypeHint = Int + case "string": + pathType = "" + pathTypeHint = String + case "array": + pathType = []interface{}{} + if schema.Items != nil { + var itemSchemas []*jsonschema.Schema + switch t := schema.Items.(type) { + case []*jsonschema.Schema: + itemSchemas = t + case *jsonschema.Schema: + itemSchemas = []*jsonschema.Schema{t} + } + var types []string + for _, is := range itemSchemas { + types = append(types, is.Types...) + if is.Ref != nil { + types = append(types, is.Ref.Types...) + } + } + types = slices.Compact(types) + if len(types) == 1 { + switch types[0] { + case "boolean": + pathType = []bool{} + pathTypeHint = BoolSlice + case "number": + pathType = []float64{} + pathTypeHint = FloatSlice + case "integer": + pathType = []float64{} + pathTypeHint = IntSlice + case "string": + pathType = []string{} + pathTypeHint = StringSlice + default: + pathType = []interface{}{} + pathTypeHint = JSON + } + } + } + case "object": + pathType = map[string]interface{}{} + pathTypeHint = JSON + } + } else if len(schema.Types) > 2 { + pathType = nil + pathTypeHint = JSON + } + + var def interface{} = schema.Default + if v, ok := def.(json.Number); ok { + def, _ = v.Float64() + } + + if (pathType != nil || schema.Default != nil) && len(parents) > 0 { + name := parents[len(parents)-1] + var required bool + if parent != nil { + for _, r := range parent.Required { + if r == name { + required = true + break + } + } + } + + path := Path{ + Name: strings.Join(parents, "."), + Default: def, + Type: pathType, + TypeHint: pathTypeHint, + Format: schema.Format, + Pattern: schema.Pattern, + Enum: schema.Enum, + Constant: schema.Constant, + MinLength: schema.MinLength, + MaxLength: schema.MaxLength, + Minimum: schema.Minimum, + Maximum: schema.Maximum, + MultipleOf: schema.MultipleOf, + ReadOnly: schema.ReadOnly, + Title: schema.Title, + Description: schema.Description, + Examples: schema.Examples, + Required: required, + } + + for _, e := range schema.Extensions { + if enhancer, ok := e.(PathEnhancer); ok { + path.CustomProperties = enhancer.EnhancePath(path) + } + } + paths = append(paths, path) + } + + if isCircular { + if maxRecursion == -1 { + return nil, errors.Errorf("detected circular dependency in schema path: %s", strings.Join(parents, ".")) + } else if currentRecursion > maxRecursion { + return paths, nil + } + currentRecursion++ + } + + if schema.Ref != nil { + path, err := listPaths(schema.Ref, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Not != nil { + path, err := listPaths(schema.Not, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.If != nil { + path, err := listPaths(schema.If, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Then != nil { + path, err := listPaths(schema.Then, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Else != nil { + path, err := listPaths(schema.Else, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.AllOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.AnyOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for _, sub := range schema.OneOf { + path, err := listPaths(sub, schema, parents, appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + for name, sub := range schema.Properties { + path, err := listPaths(sub, schema, append(parents, name), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + + if schema.Items != nil && includeArrays { + switch t := schema.Items.(type) { + case []*jsonschema.Schema: + for _, sub := range t { + path, err := listPaths(sub, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + case *jsonschema.Schema: + path, err := listPaths(t, schema, append(parents, "#"), appendPointer(pointers, schema), currentRecursion, maxRecursion, includeArrays) + if err != nil { + return nil, err + } + paths = append(paths, path...) + } + } + + return paths, nil +} diff --git a/oryx/jsonschemax/pointer.go b/oryx/jsonschemax/pointer.go new file mode 100644 index 0000000..f0c279f --- /dev/null +++ b/oryx/jsonschemax/pointer.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "net/url" + "strings" + + "github.com/pkg/errors" +) + +// JSONPointerToDotNotation converts JSON Pointer "#/foo/bar" to dot-notation "foo.bar". +func JSONPointerToDotNotation(pointer string) (string, error) { + if !strings.HasPrefix(pointer, "#/") { + return pointer, errors.Errorf("remote JSON pointers are not supported: %s", pointer) + } + + var path []string + for _, item := range strings.Split(strings.TrimPrefix(pointer, "#/"), "/") { + item = strings.Replace(item, "~1", "/", -1) + item = strings.Replace(item, "~0", "~", -1) + item, err := url.PathUnescape(item) + if err != nil { + return "", err + } + path = append(path, strings.ReplaceAll(item, ".", "\\.")) + } + + return strings.Join(path, "."), nil +} diff --git a/oryx/jsonschemax/print.go b/oryx/jsonschemax/print.go new file mode 100644 index 0000000..45c1b91 --- /dev/null +++ b/oryx/jsonschemax/print.go @@ -0,0 +1,72 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonschemax + +import ( + "errors" + "fmt" + "io" + "strings" + + "github.com/tidwall/gjson" + + "github.com/ory/jsonschema/v3" +) + +func FormatValidationErrorForCLI(w io.Writer, conf []byte, err error) { + if err == nil { + return + } + + if e := new(jsonschema.ValidationError); errors.As(err, &e) { + _, _ = fmt.Fprintln(w, "The configuration contains values or keys which are invalid:") + pointer, validation := FormatError(e) + + if pointer == "#" { + if len(e.Causes) == 0 { + _, _ = fmt.Fprintln(w, "(root)") + _, _ = fmt.Fprintln(w, "^-- "+validation) + _, _ = fmt.Fprintln(w, "") + } + } else { + spaces := make([]string, len(pointer)+3) + _, _ = fmt.Fprintf(w, "%s: %+v", pointer, gjson.GetBytes(conf, pointer).Value()) + _, _ = fmt.Fprintln(w, "") + _, _ = fmt.Fprintf(w, "%s^-- %s", strings.Join(spaces, " "), validation) + _, _ = fmt.Fprintln(w, "") + _, _ = fmt.Fprintln(w, "") + } + + for _, cause := range e.Causes { + FormatValidationErrorForCLI(w, conf, cause) + } + return + } +} + +func FormatError(e *jsonschema.ValidationError) (string, string) { + var ( + err error + pointer string + message string + ) + + pointer = e.InstancePtr + message = e.Message + switch ctx := e.Context.(type) { + case *jsonschema.ValidationErrorContextRequired: + if len(ctx.Missing) > 0 { + message = "one or more required properties are missing" + pointer = ctx.Missing[0] + } + } + + // We can ignore the error as it will simply echo the pointer. + pointer, err = JSONPointerToDotNotation(pointer) + if err != nil { + pointer = e.InstancePtr + } + + return pointer, message +} diff --git a/oryx/jsonschemax/stub/.config.yaml b/oryx/jsonschemax/stub/.config.yaml new file mode 100644 index 0000000..2367e5d --- /dev/null +++ b/oryx/jsonschemax/stub/.config.yaml @@ -0,0 +1,3 @@ +dsn: memory +items: + - id: 1 diff --git a/oryx/jsonschemax/stub/.oathkeeper.schema.json b/oryx/jsonschemax/stub/.oathkeeper.schema.json new file mode 100644 index 0000000..7126e15 --- /dev/null +++ b/oryx/jsonschemax/stub/.oathkeeper.schema.json @@ -0,0 +1,1073 @@ +{ + "$id": "https://raw.githubusercontent.com/ory/oathkeeper/v0.32.1-beta.1/.schemas/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ORY Oathkeeper Configuration", + "type": "object", + "definitions": { + "tlsxSource": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "title": "Path to PEM-encoded Fle", + "type": "string", + "examples": ["path/to/file.pem"] + }, + "base64": { + "title": "Base64 Encoded Inline", + "description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.", + "type": "string", + "examples": [ + "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..." + ] + } + } + }, + "tlsx": { + "title": "HTTPS", + "description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.", + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "title": "Private Key (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + }, + "cert": { + "title": "TLS Certificate (PEM)", + "allOf": [ + { + "$ref": "#/definitions/tlsxSource" + } + ] + } + } + }, + "cors": { + "title": "Cross Origin Resource Sharing (CORS)", + "description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.", + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": false, + "title": "Enable CORS", + "description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered." + }, + "allowed_origins": { + "title": "Allowed Origins", + "description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.", + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "default": ["*"], + "uniqueItems": true, + "examples": [ + "https://example.com", + "https://*.example.com", + "https://*.foo.example.com" + ] + }, + "allowed_methods": { + "type": "array", + "title": "Allowed HTTP Methods", + "description": "A list of methods the client is allowed to use with cross-domain requests.", + "items": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "CONNECT", + "TRACE", + "PATCH" + ] + }, + "uniqueItems": true, + "default": ["GET", "POST", "PUT", "PATCH", "DELETE"] + }, + "allowed_headers": { + "description": "A list of non simple headers the client is allowed to use with cross-domain requests.", + "title": "Allowed Request HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Authorization", "Content-Type"] + }, + "exposed_headers": { + "description": "Indicates which headers are safe to expose to the API of a CORS API specification", + "title": "Allowed Response HTTP Headers", + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "uniqueItems": true, + "default": ["Content-Type"] + }, + "allow_credentials": { + "type": "boolean", + "title": "Allow HTTP Credentials", + "default": false, + "description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates." + }, + "max_age": { + "type": "number", + "default": 0, + "title": "Maximum Age", + "description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age." + }, + "debug": { + "type": "boolean", + "default": false, + "title": "Enable Debugging", + "description": "Set to true to debug server side CORS issues." + } + }, + "additionalProperties": false + }, + "handlerSwitch": { + "title": "Enabled", + "type": "boolean", + "default": false, + "examples": [true], + "description": "En-/disables this component." + }, + "scopeStrategy": { + "title": "Scope Strategy", + "type": "string", + "enum": ["hierarchic", "exact", "wildcard", "none"], + "default": "none", + "description": "Sets the strategy validation algorithm." + }, + "configAuthenticatorsAnonymous": { + "type": "object", + "title": "Anonymous Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "subject": { + "type": "string", + "title": "Anonymous Subject", + "examples": ["guest", "anon", "anonymous", "unknown"], + "default": "anonymous", + "description": "Sets the anonymous username." + } + }, + "additionalProperties": false + }, + "configAuthenticatorsCookieSession": { + "type": "object", + "title": "Cookie Session Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "check_session_url": { + "title": "Session Check URL", + "type": "string", + "format": "uri", + "description": "The origin to proxy requests to. If the response is a 200 with body `{ \"subject\": \"...\", \"extra\": {} }`. The request will pass the subject through successfully, otherwise it will be marked as unauthorized.\n\n>If this authenticator is enabled, this value is required.", + "examples": ["https://session-store-host"] + }, + "only": { + "type": "array", + "items": { + "type": "string", + "additionalItems": false + }, + "title": "Only Cookies", + "description": "A list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if none of the passed cookies are set on the request." + } + }, + "required": ["check_session_url"], + "additionalProperties": false + }, + "configAuthenticatorsJwt": { + "type": "object", + "title": "JWT Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "required": ["jwks_urls"], + "properties": { + "required_scope": { + "type": "array", + "title": "Required Token Scope", + "description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "items": { + "type": "string" + } + }, + "target_audience": { + "title": "Intended Audience", + "type": "array", + "description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "items": { + "type": "string" + } + }, + "trusted_issuers": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowed_algorithms": { + "type": "array", + "items": { + "type": "string" + } + }, + "jwks_urls": { + "title": "JSON Web Key URLs", + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "URLs where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web Token. Usually something like \"https://my-keys.com/.well-known/jwks.json\". The response of that endpoint must return a JSON Web Key Set (JWKS).\n\n>If this authenticator is enabled, this value is required.", + "examples": [ + "https://my-website.com/.well-known/jwks.json", + "https://my-other-website.com/.well-known/jwks.json", + "file://path/to/local/jwks.json" + ] + }, + "scope_strategy": { + "$ref": "#/definitions/scopeStrategy" + }, + "token_from": { + "title": "Token From", + "description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "oneOf": [ + { + "type": "object", + "required": ["header"], + "properties": { + "header": { + "title": "Header", + "type": "string", + "description": "The header (case insensitive) that must contain a token for request authentication. It can't be set along with query_parameter." + } + } + }, + { + "type": "object", + "required": ["query_parameter"], + "properties": { + "query_parameter": { + "title": "Query Parameter", + "type": "string", + "description": "The query parameter (case sensitive) that must contain a token for request authentication. It can't be set along with header." + } + } + } + ] + } + }, + "additionalProperties": false + }, + "configAuthenticatorsOauth2ClientCredentials": { + "type": "object", + "title": "OAuth 2.0 Client Credentials Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "token_url": { + "type": "string", + "description": "The OAuth 2.0 Token Endpoint that will be used to validate the client credentials.\n\n>If this authenticator is enabled, this value is required.", + "format": "uri", + "examples": ["https://my-website.com/oauth2/token"] + }, + "required_scope": { + "type": "array", + "title": "Request Permissions (Token Scope)", + "description": "Scopes is an array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this rule.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "items": { + "type": "string" + } + } + }, + "required": ["token_url"], + "additionalProperties": false + }, + "configAuthenticatorsOauth2Introspection": { + "type": "object", + "title": "OAuth 2.0 Introspection Authenticator Configuration", + "description": "This section is optional when the authenticator is disabled.", + "properties": { + "introspection_url": { + "type": "string", + "format": "uri", + "examples": ["https://my-website.com/oauth2/introspection"], + "title": "OAuth 2.0 Introspection URL", + "description": "The OAuth 2.0 Token Introspection endpoint URL.\n\n>If this authenticator is enabled, this value is required." + }, + "scope_strategy": { + "$ref": "#/definitions/scopeStrategy" + }, + "pre_authorization": { + "title": "Pre-Authorization", + "description": "Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.", + "type": "object", + "additionalProperties": false, + "required": ["client_id", "client_secret", "token_url"], + "properties": { + "enabled": { + "const": true + }, + "client_id": { + "type": "string", + "title": "OAuth 2.0 Client ID", + "description": "The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant.\n\n>If pre-authorization is enabled, this value is required." + }, + "client_secret": { + "type": "string", + "title": "OAuth 2.0 Client Secret", + "description": "The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant.\n\n>If pre-authorization is enabled, this value is required." + }, + "token_url": { + "type": "string", + "format": "uri", + "title": "OAuth 2.0 Token URL", + "description": "The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed.\n\n>If pre-authorization is enabled, this value is required." + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "title": "OAuth 2.0 Scope", + "description": "The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant.", + "examples": [["[\"foo\", \"bar\"]"]] + } + } + }, + "required_scope": { + "title": "Required Scope", + "description": "An array of OAuth 2.0 scopes that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header did not request that specific scope, the request is denied.", + "type": "array", + "items": { + "type": "string" + } + }, + "target_audience": { + "title": "Target Audience", + "description": "An array of audiences that are required when accessing an endpoint protected by this handler.\n If the token used in the Authorization header is not intended for any of the requested audiences, the request is denied.", + "type": "array", + "items": { + "type": "string" + } + }, + "trusted_issuers": { + "title": "Trusted Issuers", + "description": "The token must have been issued by one of the issuers listed in this array.", + "type": "array", + "items": { + "type": "string" + } + }, + "token_from": { + "title": "Token From", + "description": "The location of the token.\n If not configured, the token will be received from a default location - 'Authorization' header.\n One and only one location (header or query) must be specified.", + "type": "object", + "oneOf": [ + { + "required": ["header"], + "properties": { + "header": { + "title": "Header", + "type": "string", + "description": "The header (case insensitive) that must contain a token for request authentication.\n It can't be set along with query_parameter." + } + } + }, + { + "required": ["query_parameter"], + "properties": { + "query_parameter": { + "title": "Query Parameter", + "type": "string", + "description": "The query parameter (case sensitive) that must contain a token for request authentication.\n It can't be set along with header." + } + } + } + ] + } + }, + "required": ["introspection_url"], + "additionalProperties": false + }, + "configAuthorizersKetoEngineAcpOry": { + "type": "object", + "title": "ORY Keto Access Control Policy Authorizer Configuration", + "description": "This section is optional when the authorizer is disabled.", + "properties": { + "base_url": { + "title": "Base URL", + "type": "string", + "format": "uri", + "description": "The base URL of ORY Keto.\n\n>If this authorizer is enabled, this value is required.", + "examples": ["http://my-keto/"] + }, + "required_action": { + "type": "string", + "default": "unset" + }, + "required_resource": { + "type": "string", + "default": "unset" + }, + "subject": { + "type": "string" + }, + "flavor": { + "type": "string" + } + }, + "required": ["base_url", "required_action", "required_resource"], + "additionalProperties": false + }, + "configMutatorsCookie": { + "type": "object", + "title": "Cookie Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["cookies"], + "properties": { + "cookies": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "configMutatorsHeader": { + "type": "object", + "title": "Header Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["headers"], + "properties": { + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "configMutatorsHydrator": { + "type": "object", + "title": "Hydrator Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "properties": { + "api": { + "additionalProperties": false, + "required": ["url"], + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "auth": { + "type": "object", + "additionalProperties": false, + "properties": { + "basic": { + "required": ["username", "password"], + "type": "object", + "additionalProperties": false, + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + } + } + }, + "retry": { + "type": "object", + "additionalProperties": false, + "properties": { + "number_of_retries": { + "type": "number", + "minimum": 0, + "default": 100 + }, + "delay_in_milliseconds": { + "type": "integer", + "minimum": 0, + "default": 3 + } + } + } + } + } + }, + "required": ["api"], + "additionalProperties": false + }, + "configMutatorsIdToken": { + "type": "object", + "title": "ID Token Mutator Configuration", + "description": "This section is optional when the mutator is disabled.", + "required": ["jwks_url", "issuer_url"], + "properties": { + "claims": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "title": "Issuer URL", + "description": "Sets the \"iss\" value of the ID Token.\n\n>If this mutator is enabled, this value is required." + }, + "jwks_url": { + "type": "string", + "format": "uri", + "title": "JSON Web Key URL", + "description": "Sets the URL where keys should be fetched from. Supports remote locations (http, https) as well as local filesystem paths.\n\n>If this mutator is enabled, this value is required.", + "examples": [ + "https://fetch-keys/from/this/location.json", + "file:///from/this/absolute/location.json", + "file://../from/this/relative/location.json" + ] + }, + "ttl": { + "type": "string", + "title": "Expire After", + "description": "Sets the time-to-live of the JSON Web Token.", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "default": "1m", + "examples": ["1h", "1m", "30s"] + } + }, + "additionalProperties": false + } + }, + "properties": { + "serve": { + "title": "HTTP(s)", + "additionalProperties": false, + "type": "object", + "properties": { + "api": { + "type": "object", + "title": "HTTP REST API", + "additionalProperties": false, + "properties": { + "port": { + "type": "number", + "default": 4456, + "title": "Port", + "description": "The port to listen on." + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on." + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + }, + "proxy": { + "type": "object", + "title": "HTTP Reverse Proxy", + "additionalProperties": false, + "properties": { + "port": { + "type": "number", + "default": 4455, + "title": "Port", + "description": "The port to listen on." + }, + "host": { + "type": "string", + "default": "", + "examples": ["localhost", "127.0.0.1"], + "title": "Host", + "description": "The network interface to listen on. Leave empty to listen on all interfaces." + }, + "timeout": { + "title": "HTTP Timeouts", + "description": "Control the reverse proxy's HTTP timeouts.", + "type": "object", + "additionalProperties": false, + "properties": { + "read": { + "title": "HTTP Read Timeout", + "type": "string", + "default": "5s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": "The maximum duration for reading the entire request, including the body.", + "examples": ["5s", "5m", "5h"] + }, + "write": { + "title": "HTTP Write Timeout", + "type": "string", + "default": "120s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": "The maximum duration before timing out writes of the response. Increase this parameter to prevent unexpected closing a client connection if an upstream request is responding slowly.", + "examples": ["5s", "5m", "5h"] + }, + "idle": { + "title": "HTTP Idle Timeout", + "type": "string", + "default": "120s", + "pattern": "^[0-9]+(ns|us|ms|s|m|h)$", + "description": " The maximum amount of time to wait for any action of a request session, reading data or writing the response.", + "examples": ["5s", "5m", "5h"] + } + } + }, + "cors": { + "$ref": "#/definitions/cors" + }, + "tls": { + "$ref": "#/definitions/tlsx" + } + } + } + } + }, + "access_rules": { + "title": "Access Rules", + "description": "Configure access rules. All sub-keys support configuration reloading without restarting.", + "type": "object", + "additionalProperties": false, + "properties": { + "repositories": { + "title": "Repositories", + "description": "Locations (list of URLs) where access rules should be fetched from on boot. It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules:\n\n- If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be fetched from the local file system.\n- If the URL Scheme is `inline://`, the access rules (an array of access rules is expected) are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{\"id\":\"foo-rule\",\"authenticators\":[....]}]`)).\n- If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be fetched from the provided HTTP(s) location.", + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "examples": [ + "[\"file://path/to/rules.json\",\"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d\",\"https://path-to-my-rules/rules.json\"]" + ] + } + } + }, + "authenticators": { + "title": "Authenticators", + "type": "object", + "description": "For more information on authenticators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authn", + "additionalProperties": false, + "properties": { + "anonymous": { + "title": "Anonymous", + "description": "The [`anonymous` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#anonymous).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsAnonymous" + } + } + }, + "noop": { + "title": "No Operation (noop)", + "description": "The [`noop` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#noop).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "unauthorized": { + "title": "Unauthorized", + "description": "The [`unauthorized` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#unauthorized).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "cookie_session": { + "title": "Cookie Session", + "description": "The [`cookie_session` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#cookie_session).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsCookieSession" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "jwt": { + "title": "JSON Web Token (jwt)", + "description": "The [`jwt` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#jwt).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsJwt" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "oauth2_client_credentials": { + "title": "OAuth 2.0 Client Credentials", + "description": "The [`oauth2_client_credentials` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_client_credentials).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsOauth2ClientCredentials" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "oauth2_introspection": { + "title": "OAuth 2.0 Token Introspection", + "description": "The [`oauth2_introspection` authenticator](https://www.ory.sh/docs/oathkeeper/pipeline/authn#oauth2_introspection).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthenticatorsOauth2Introspection" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "authorizers": { + "title": "Authorizers", + "type": "object", + "description": "For more information on authorizers head over to: https://www.ory.sh/docs/oathkeeper/pipeline/authz", + "additionalProperties": false, + "properties": { + "allow": { + "title": "Allow", + "description": "The [`allow` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "deny": { + "title": "Deny", + "description": "The [`deny` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#allow).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "keto_engine_acp_ory": { + "title": "ORY Keto Access Control Policies Engine", + "description": "The [`keto_engine_acp_ory` authorizer](https://www.ory.sh/docs/oathkeeper/pipeline/authz#keto_engine_acp_ory).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configAuthorizersKetoEngineAcpOry" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "mutators": { + "title": "Mutators", + "type": "object", + "description": "For more information on mutators head over to: https://www.ory.sh/docs/oathkeeper/pipeline/mutator", + "additionalProperties": false, + "properties": { + "noop": { + "title": "No Operation (noop)", + "description": "The [`noop` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#noop).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + } + } + }, + "cookie": { + "title": "HTTP Cookie", + "description": "The [`cookie` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#cookie).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsCookie" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "header": { + "title": "HTTP Header", + "description": "The [`header` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#header).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsHeader" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "hydrator": { + "title": "Hydrator", + "description": "The [`hydrator` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#hydrator).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsHydrator" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + }, + "id_token": { + "title": "ID Token (JSON Web Token)", + "description": "The [`id_token` mutator](https://www.ory.sh/docs/oathkeeper/pipeline/mutator#id_token).", + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "$ref": "#/definitions/handlerSwitch" + }, + "config": { + "$ref": "#/definitions/configMutatorsIdToken" + } + }, + "oneOf": [ + { + "properties": { + "enabled": { + "const": true + } + }, + "required": ["config"] + }, + { + "properties": { + "enabled": { + "const": false + } + } + } + ] + } + } + }, + "log": { + "title": "Log", + "description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "type": "string", + "default": "info", + "enum": ["panic", "fatal", "error", "warn", "info", "debug"], + "title": "Level", + "description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL." + }, + "format": { + "type": "string", + "default": "text", + "enum": ["text", "json"], + "title": "Format", + "description": "The log format can either be text or JSON." + } + }, + "additionalProperties": false + }, + "profiling": { + "title": "Profiling", + "description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).", + "type": "string", + "enum": ["cpu", "mem"] + } + }, + "required": [], + "additionalProperties": false +} diff --git a/oryx/jsonschemax/stub/config.schema.json b/oryx/jsonschemax/stub/config.schema.json new file mode 100644 index 0000000..537e6ac --- /dev/null +++ b/oryx/jsonschemax/stub/config.schema.json @@ -0,0 +1,12 @@ +{ + "$id": "https://example.com/config.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "config", + "type": "object", + "properties": { + "dsn": { + "type": "string" + } + }, + "required": ["dsn"] +} diff --git a/oryx/jsonschemax/stub/json/.project-stub-name.json b/oryx/jsonschemax/stub/json/.project-stub-name.json new file mode 100644 index 0000000..798f6eb --- /dev/null +++ b/oryx/jsonschemax/stub/json/.project-stub-name.json @@ -0,0 +1,7 @@ +{ + "serve": { + "admin": { + "port": 1 + } + } +} diff --git a/oryx/jsonschemax/stub/nested-array.schema.json b/oryx/jsonschemax/stub/nested-array.schema.json new file mode 100644 index 0000000..b70c935 --- /dev/null +++ b/oryx/jsonschemax/stub/nested-array.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "title": "OpenID Connect and OAuth2 Providers", + "description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "examples": ["google"] + }, + "provider": { + "title": "Provider", + "description": "Can be one of github, gitlab, generic, google, microsoft, discord.", + "type": "string", + "enum": [ + "github", + "gitlab", + "generic", + "google", + "microsoft", + "discord" + ], + "examples": ["google"] + }, + "client_id": { + "type": "string" + }, + "client_secret": { + "type": "string" + }, + "issuer_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com"] + }, + "auth_url": { + "type": "string", + "format": "uri", + "examples": ["https://accounts.google.com/o/oauth2/v2/auth"] + }, + "token_url": { + "type": "string", + "format": "uri", + "examples": ["https://www.googleapis.com/oauth2/v4/token"] + }, + "mapper_url": { + "title": "Jsonnet Mapper URL", + "description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.", + "type": "string", + "format": "uri", + "examples": [ + "file://path/to/oidc.jsonnet", + "https://foo.bar.com/path/to/oidc.jsonnet", + "base64://bG9jYWwgc3ViamVjdCA9I..." + ] + }, + "scope": { + "type": "array", + "items": { + "type": "string", + "examples": ["offline_access", "profile"] + } + }, + "tenant": { + "title": "Azure AD Tenant", + "description": "The Azure AD Tenant to use for authentication.", + "type": "string", + "examples": [ + "common", + "organizations", + "consumers", + "8eaef023-2b34-4da1-9baa-8bc8c9d6a490", + "contoso.onmicrosoft.com" + ] + } + }, + "additionalProperties": false, + "required": [], + "if": { + "properties": { + "provider": { + "const": "microsoft" + } + }, + "required": ["provider"] + }, + "then": { + "required": ["tenant"] + }, + "else": { + "not": { + "properties": { + "tenant": {} + }, + "required": ["tenant"] + } + } + } + } + } +} diff --git a/oryx/jsonschemax/stub/nested-simple-array.schema.json b/oryx/jsonschemax/stub/nested-simple-array.schema.json new file mode 100644 index 0000000..7bfe7a8 --- /dev/null +++ b/oryx/jsonschemax/stub/nested-simple-array.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + } + } + } +} diff --git a/oryx/jsonschemax/stub/toml/.project-stub-name.toml b/oryx/jsonschemax/stub/toml/.project-stub-name.toml new file mode 100644 index 0000000..5db36ed --- /dev/null +++ b/oryx/jsonschemax/stub/toml/.project-stub-name.toml @@ -0,0 +1,4 @@ +[serve] + + [serve.admin] + port = "2" \ No newline at end of file diff --git a/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml b/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml new file mode 100644 index 0000000..f41a4a0 --- /dev/null +++ b/oryx/jsonschemax/stub/yaml/.project-stub-name.yaml @@ -0,0 +1,4 @@ +# serve controls the configuration for the http(s) daemon +serve: + admin: + port: 3 diff --git a/oryx/jsonschemax/stub/yml/.project-stub-name.yml b/oryx/jsonschemax/stub/yml/.project-stub-name.yml new file mode 100644 index 0000000..ccd7c3b --- /dev/null +++ b/oryx/jsonschemax/stub/yml/.project-stub-name.yml @@ -0,0 +1,3 @@ +serve: + admin: + port: 4 diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=1.json.json @@ -0,0 +1 @@ +"foo" diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json new file mode 100644 index 0000000..fab1a3b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=2.json.json @@ -0,0 +1,3 @@ +{ + "some": "key" +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json new file mode 100644 index 0000000..7306c23 --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=3.json.json @@ -0,0 +1,3 @@ +{ + "some_key": 1234 +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json new file mode 100644 index 0000000..2d0d20a --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=4.json.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "source": "base64://aGVsbG8gd29ybGQ=" + }, + "array": [ + { + "nested": { + "source": "base64://aGVsbG8gd29ybGQ=" + } + }, + "base64://aGVsbG8gd29ybGQ=" + ] + } +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json new file mode 100644 index 0000000..bfa283b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=5.json.json @@ -0,0 +1 @@ +"https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json new file mode 100644 index 0000000..a167c0b --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-fixtures-fixture=6.json.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "ignore_this_key": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "base64://aGVsbG8gd29ybGQ=" + } + }, + "base64://aGVsbG8gd29ybGQ=" + ] + } +} diff --git a/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json b/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json new file mode 100644 index 0000000..f056437 --- /dev/null +++ b/oryx/jsonx/.snapshots/TestEmbedSources-only_embeds_base64.json @@ -0,0 +1,4 @@ +{ + "key": "https://foobar.com", + "bar": "base64://YXNkZg==" +} diff --git a/oryx/jsonx/debug.go b/oryx/jsonx/debug.go new file mode 100644 index 0000000..9b738f1 --- /dev/null +++ b/oryx/jsonx/debug.go @@ -0,0 +1,76 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "fmt" + "slices" +) + +// Anonymize takes a JSON byte array and anonymizes its content by +// recursively replacing all values with a string indicating their type. +// +// It recurses into nested objects and arrays, but ignores the "schemas" and "id". +func Anonymize(data []byte, except ...string) []byte { + obj := make(map[string]any) + if err := json.Unmarshal(data, &obj); err != nil { + return []byte(fmt.Sprintf(`{"error": "invalid JSON", "message": %q}`, err.Error())) + } + + anonymize(obj, except...) + + out, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return []byte(fmt.Sprintf(`{"error": "could not marshal JSON shape", "message": %q}`, err.Error())) + } + + return out +} + +func anonymize(obj map[string]any, except ...string) { + for k, v := range obj { + if slices.Contains(except, k) { + continue + } + + switch v := v.(type) { + case []any: + for elIdx, el := range v { + switch el := el.(type) { + case map[string]any: + anonymize(el) + v[elIdx] = el + default: + v[elIdx] = jsonType(el) + } + } + + case map[string]any: + anonymize(v) + obj[k] = v + default: + obj[k] = jsonType(v) + } + } +} + +func jsonType(v any) string { + switch v := v.(type) { + case string: + return "string" + case float64: + return "number" + case bool: + return "boolean" + case nil: + return "null" + case []any: + return "array" + case map[string]any: + return "object" + default: + return fmt.Sprintf("%T", v) + } +} diff --git a/oryx/jsonx/decoder.go b/oryx/jsonx/decoder.go new file mode 100644 index 0000000..d7a00a1 --- /dev/null +++ b/oryx/jsonx/decoder.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "io" +) + +// NewStrictDecoder is a shorthand for json.Decoder.DisallowUnknownFields +func NewStrictDecoder(b io.Reader) *json.Decoder { + d := json.NewDecoder(b) + d.DisallowUnknownFields() + return d +} diff --git a/oryx/jsonx/embed.go b/oryx/jsonx/embed.go new file mode 100644 index 0000000..ae4f94c --- /dev/null +++ b/oryx/jsonx/embed.go @@ -0,0 +1,113 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/base64" + "encoding/json" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + + "github.com/ory/x/osx" +) + +type options struct { + ignoreKeys []string + onlySchemes []string +} + +type OptionsModifier func(*options) + +func newOptions(o []OptionsModifier) *options { + opt := &options{} + for _, f := range o { + f(opt) + } + return opt +} + +func WithIgnoreKeys(keys ...string) OptionsModifier { + return func(o *options) { + o.ignoreKeys = keys + } +} + +func WithOnlySchemes(scheme ...string) OptionsModifier { + return func(o *options) { + o.onlySchemes = scheme + } +} + +func EmbedSources(in json.RawMessage, opts ...OptionsModifier) (out json.RawMessage, err error) { + out = make([]byte, len(in)) + copy(out, in) + if err := embed(gjson.ParseBytes(in), nil, &out, newOptions(opts)); err != nil { + return nil, err + } + return out, nil +} + +func embed(parsed gjson.Result, parents []string, result *json.RawMessage, o *options) (err error) { + if parsed.IsObject() { + parsed.ForEach(func(k, v gjson.Result) bool { + err = embed(v, append(parents, strings.ReplaceAll(k.String(), ".", "\\.")), result, o) + return err == nil + }) + if err != nil { + return err + } + } else if parsed.IsArray() { + for kk, vv := range parsed.Array() { + if err = embed(vv, append(parents, strconv.Itoa(kk)), result, o); err != nil { + return err + } + } + } else if parsed.Type != gjson.String { + return nil + } + + if len(parents) > 0 && slices.Contains(o.ignoreKeys, parents[len(parents)-1]) { + return nil + } + + loc, err := url.ParseRequestURI(parsed.String()) + if err != nil { + // Not a URL, return + return nil + } + + if len(o.onlySchemes) == 0 { + if loc.Scheme != "file" && loc.Scheme != "http" && loc.Scheme != "https" && loc.Scheme != "base64" { + // Not a known pattern, ignore + return nil + } + } else if !slices.Contains(o.onlySchemes, loc.Scheme) { + // Not a known pattern, ignore + return nil + } + + contents, err := osx.ReadFileFromAllSources(loc.String()) + if err != nil { + return err + } + + encoded := base64.StdEncoding.EncodeToString(contents) + key := strings.Join(parents, ".") + if key == "" { + key = "@" + } + + interim, err := sjson.SetBytes(*result, key, "base64://"+encoded) + if err != nil { + return err + } + + *result = interim + return +} diff --git a/oryx/jsonx/fixture/embed/1.json b/oryx/jsonx/fixture/embed/1.json new file mode 100644 index 0000000..810c96e --- /dev/null +++ b/oryx/jsonx/fixture/embed/1.json @@ -0,0 +1 @@ +"foo" diff --git a/oryx/jsonx/fixture/embed/2.json b/oryx/jsonx/fixture/embed/2.json new file mode 100644 index 0000000..fab1a3b --- /dev/null +++ b/oryx/jsonx/fixture/embed/2.json @@ -0,0 +1,3 @@ +{ + "some": "key" +} diff --git a/oryx/jsonx/fixture/embed/3.json b/oryx/jsonx/fixture/embed/3.json new file mode 100644 index 0000000..7306c23 --- /dev/null +++ b/oryx/jsonx/fixture/embed/3.json @@ -0,0 +1,3 @@ +{ + "some_key": 1234 +} diff --git a/oryx/jsonx/fixture/embed/4.json b/oryx/jsonx/fixture/embed/4.json new file mode 100644 index 0000000..279ceab --- /dev/null +++ b/oryx/jsonx/fixture/embed/4.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + } + }, + "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + ] + } +} diff --git a/oryx/jsonx/fixture/embed/5.json b/oryx/jsonx/fixture/embed/5.json new file mode 100644 index 0000000..bfa283b --- /dev/null +++ b/oryx/jsonx/fixture/embed/5.json @@ -0,0 +1 @@ +"https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" diff --git a/oryx/jsonx/fixture/embed/6.json b/oryx/jsonx/fixture/embed/6.json new file mode 100644 index 0000000..1fde753 --- /dev/null +++ b/oryx/jsonx/fixture/embed/6.json @@ -0,0 +1,15 @@ +{ + "nested": { + "object": { + "ignore_this_key": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + }, + "array": [ + { + "nested": { + "source": "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + } + }, + "https://gist.githubusercontent.com/aeneasr/eb4612d295f613ee44bada6e30e2a856/raw/29edbda41bcb27492a1ac56926e03dee9480708f/hello-world.txt" + ] + } +} diff --git a/oryx/jsonx/flatten.go b/oryx/jsonx/flatten.go new file mode 100644 index 0000000..e4e04ba --- /dev/null +++ b/oryx/jsonx/flatten.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/tidwall/gjson" +) + +// Flatten flattens a JSON object using dot notation. +func Flatten(raw json.RawMessage) map[string]interface{} { + parsed := gjson.ParseBytes(raw) + if !parsed.IsObject() { + return nil + } + + flattened := make(map[string]interface{}) + flatten(parsed, nil, flattened) + return flattened +} + +func flatten(parsed gjson.Result, parents []string, flattened map[string]interface{}) { + if parsed.IsObject() { + parsed.ForEach(func(k, v gjson.Result) bool { + flatten(v, append(parents, strings.ReplaceAll(k.String(), ".", "\\.")), flattened) + return true + }) + } else if parsed.IsArray() { + for kk, vv := range parsed.Array() { + flatten(vv, append(parents, strconv.Itoa(kk)), flattened) + } + } else { + flattened[strings.Join(parents, ".")] = parsed.Value() + } +} diff --git a/oryx/jsonx/get.go b/oryx/jsonx/get.go new file mode 100644 index 0000000..24129d9 --- /dev/null +++ b/oryx/jsonx/get.go @@ -0,0 +1,77 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "reflect" + "strings" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func jsonKey(f reflect.StructField) *string { + if jsonTag := f.Tag.Get("json"); jsonTag != "" { + if jsonTag == "-" { + return nil + } + return &strings.Split(jsonTag, ",")[0] + } else if f.Anonymous { + return nil + } else if f.IsExported() { + return &f.Name + } + return nil +} + +// AllValidJSONKeys returns all JSON keys from the struct or *struct type. +// It does not return keys from nested slices, but embedded/nested structs. +func AllValidJSONKeys(s interface{}) (keys []string) { + t := reflect.TypeOf(s) + v := reflect.ValueOf(s) + if t.Kind() == reflect.Pointer { + t = t.Elem() + v = v.Elem() + } + for i := range t.NumField() { + f := t.Field(i) + jKey := jsonKey(f) + if k := f.Type.Kind(); k == reflect.Struct || k == reflect.Pointer { + subKeys := AllValidJSONKeys(v.Field(i).Interface()) + for _, subKey := range subKeys { + if jKey != nil { + keys = append(keys, *jKey+"."+subKey) + } else { + keys = append(keys, subKey) + } + } + } else if jKey != nil { + keys = append(keys, *jKey) + } + } + return keys +} + +// ParseEnsureKeys returns a result that has the GetRequireValidKey function. +func ParseEnsureKeys(original interface{}, raw []byte) *Result { + return &Result{ + keys: AllValidJSONKeys(original), + result: gjson.ParseBytes(raw), + } +} + +type Result struct { + result gjson.Result + keys []string +} + +// GetRequireValidKey ensures that the key is valid before returning the result. +func (r *Result) GetRequireValidKey(t require.TestingT, key string) gjson.Result { + require.Contains(t, r.keys, key) + return r.result.Get(key) +} + +func GetRequireValidKey(t require.TestingT, original interface{}, raw []byte, key string) gjson.Result { + return ParseEnsureKeys(original, raw).GetRequireValidKey(t, key) +} diff --git a/oryx/jsonx/helpers.go b/oryx/jsonx/helpers.go new file mode 100644 index 0000000..26ff3c7 --- /dev/null +++ b/oryx/jsonx/helpers.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshalJSONString(t *testing.T, i interface{}) string { + out, err := json.Marshal(i) + require.NoError(t, err) + return string(out) +} diff --git a/oryx/jsonx/patch.go b/oryx/jsonx/patch.go new file mode 100644 index 0000000..6fd185c --- /dev/null +++ b/oryx/jsonx/patch.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jsonx + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + jsonpatch "github.com/evanphx/json-patch/v5" + "github.com/gobwas/glob" + "github.com/pkg/errors" +) + +var opAllowList = map[string]struct{}{ + "add": {}, + "remove": {}, + "replace": {}, +} + +func isUnsupported(op jsonpatch.Operation) bool { + _, ok := opAllowList[op.Kind()] + + return !ok +} + +func isElementAccess(path string) bool { + if path == "" { + return false + } + elements := strings.Split(path, "/") + lastElement := elements[len(elements)-1:][0] + if lastElement == "-" { + return true + } + if _, err := strconv.Atoi(lastElement); err == nil { + return true + } + + return false +} + +// ApplyJSONPatch applies a JSON patch to an object and returns the modified +// object. The original object is not modified. It returns an error if the patch +// is invalid or if the patch includes paths that are denied. denyPaths is a +// list of path globs (interpreted with [glob.Compile] that are not allowed to +// be patched. +func ApplyJSONPatch[T any](p json.RawMessage, object T, denyPaths ...string) (result T, err error) { + patch, err := jsonpatch.DecodePatch(p) + if err != nil { + return result, errors.WithStack(err) + } + + denyPattern := fmt.Sprintf("{%s}", strings.ToLower(strings.Join(denyPaths, ","))) + matcher, err := glob.Compile(denyPattern, '/') + if err != nil { + return result, errors.WithStack(err) + } + + for _, op := range patch { + // Some operations are buggy, see https://github.com/evanphx/json-patch/pull/158 + if isUnsupported(op) { + return result, errors.Errorf("unsupported operation: %s", op.Kind()) + } + path, err := op.Path() + if err != nil { + return result, errors.Errorf("error parsing patch operations: %v", err) + } + if matcher.Match(strings.ToLower(path)) { + return result, errors.Errorf("patch includes denied path: %s", path) + } + + // JSON patch officially rejects replacing paths that don't exist, but we want to be more tolerant. + // Therefore, we will ensure that all paths that we want to replace exist in the original document. + if op.Kind() == "replace" && !isElementAccess(path) { + op["op"] = new(json.RawMessage(`"add"`)) + } + } + + original, err := json.Marshal(object) + if err != nil { + return result, errors.WithStack(err) + } + + options := jsonpatch.NewApplyOptions() + options.EnsurePathExistsOnAdd = true + + modified, err := patch.ApplyWithOptions(original, options) + if err != nil { + return result, errors.WithStack(err) + } + + err = json.Unmarshal(modified, &result) + return result, errors.WithStack(err) +} diff --git a/oryx/jsonx/stub/random.json b/oryx/jsonx/stub/random.json new file mode 100644 index 0000000..101ee9d --- /dev/null +++ b/oryx/jsonx/stub/random.json @@ -0,0 +1,64 @@ +{ + "floating": [ + -1273085434, + 953442581, + { + "ready": "silent", + "worker": "situation", + "joy": "difference", + "probably": -413625494, + "gray": { + "parent": "pull", + "shore": -738396277, + "usually": 1050049449, + "hold": [ + [ + 181518765, + [ + { + "steam": { + "box": false, + "cry": 1463961818, + "appropriate": 1249911539, + "through": 695239749, + "ago": true, + "entirely": -851427469 + }, + "leather": "across", + "flies": -1571371799, + "over": 512666854, + "thank": true, + "shaking": true + }, + "hit", + -648178744.4899056, + 1225027271, + -1481507228, + true + ], + -2114582277, + 1390060204.9360588, + 1615602630.9049141, + "darkness" + ], + 63427197.713988304, + -580344963.961421, + "stems", + 1016960217.612642, + 1240918909 + ], + "buy": true, + "wonder": false + }, + "little": "cloud" + }, + "grade", + false, + "thou" + ], + "wagon": -1722583702, + "shop": 1294397217, + "spend": "greatest", + "product": "whale", + "fall": "to" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json new file mode 100644 index 0000000..ecb9a86 --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_multiple_source_urls-case=succeeds_with_forced_kid.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "8d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_cache_and_TTL.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=with_forced_key.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json new file mode 100644 index 0000000..f81e76c --- /dev/null +++ b/oryx/jwksx/.snapshots/TestFetcherNext-case=resolve_single_source_url-case=without_cache.json @@ -0,0 +1,7 @@ +{ + "alg": "HS256", + "k": "Y2hhbmdlbWVjaGFuZ2VtZWNoYW5nZW1lY2hhbmdlbWU", + "kid": "7d5f5ad0674ec2f2960b1a34f33370a0f71471fa0e3ef0c0a692977d276dafe8", + "kty": "oct", + "use": "sig" +} diff --git a/oryx/jwksx/fetcher.go b/oryx/jwksx/fetcher.go new file mode 100644 index 0000000..4b476a3 --- /dev/null +++ b/oryx/jwksx/fetcher.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "encoding/json" + "net/http" + "sync" + + "github.com/go-jose/go-jose/v3" + "github.com/pkg/errors" +) + +// Fetcher is a small helper for fetching JSON Web Keys from remote endpoints. +// +// DEPRECATED: Use FetcherNext instead. +type Fetcher struct { + sync.RWMutex + remote string + c *http.Client + keys map[string]jose.JSONWebKey +} + +// NewFetcher returns a new fetcher that can download JSON Web Keys from remote endpoints. +// +// DEPRECATED: Use FetcherNext instead. +func NewFetcher(remote string) *Fetcher { + return &Fetcher{ + remote: remote, + c: http.DefaultClient, + keys: make(map[string]jose.JSONWebKey), + } +} + +// GetKey retrieves a JSON Web Key from the cache, fetches it from a remote if it is not yet cached or returns an error. +// +// DEPRECATED: Use FetcherNext instead. +func (f *Fetcher) GetKey(kid string) (*jose.JSONWebKey, error) { + f.RLock() + if k, ok := f.keys[kid]; ok { + f.RUnlock() + return &k, nil + } + f.RUnlock() + + res, err := f.c.Get(f.remote) + if err != nil { + return nil, errors.WithStack(err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, errors.Errorf("expected status code 200 but got %d when requesting %s", res.StatusCode, f.remote) + } + + var set jose.JSONWebKeySet + if err := json.NewDecoder(res.Body).Decode(&set); err != nil { + return nil, errors.WithStack(err) + } + + for _, k := range set.Keys { + f.Lock() + f.keys[k.KeyID] = k + f.Unlock() + } + + f.RLock() + defer f.RUnlock() + if k, ok := f.keys[kid]; ok { + return &k, nil + } + + return nil, errors.Errorf("unable to find JSON Web Key with ID: %s", kid) +} diff --git a/oryx/jwksx/fetcher_v2.go b/oryx/jwksx/fetcher_v2.go new file mode 100644 index 0000000..1e00622 --- /dev/null +++ b/oryx/jwksx/fetcher_v2.go @@ -0,0 +1,179 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "context" + "crypto/sha256" + "time" + + "github.com/ory/herodot" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/ory/x/fetcher" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/otelx" + + "github.com/dgraph-io/ristretto/v2" + "github.com/lestrrat-go/jwx/jwk" + "github.com/pkg/errors" + "golang.org/x/sync/errgroup" +) + +var ErrUnableToFindKeyID = errors.New("specified JWK kid can not be found in the JWK sets") + +type ( + fetcherNextOptions struct { + forceKID string + cacheTTL time.Duration + useCache bool + httpClient *retryablehttp.Client + schemes []string + } + // FetcherNext is a JWK fetcher that can be used to fetch JWKs from multiple locations. + FetcherNext struct { + cache *ristretto.Cache[[]byte, jwk.Set] + } + // FetcherNextOption is a functional option for the FetcherNext. + FetcherNextOption func(*fetcherNextOptions) +) + +// NewFetcherNext returns a new FetcherNext instance. +func NewFetcherNext(cache *ristretto.Cache[[]byte, jwk.Set]) *FetcherNext { + return &FetcherNext{ + cache: cache, + } +} + +// WithForceKID forces the key ID to be used. Required when multiple JWK sets are configured. +func WithForceKID(kid string) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.forceKID = kid + } +} + +// WithCacheTTL sets the cache TTL. If not set, the TTL is unlimited. +func WithCacheTTL(ttl time.Duration) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.cacheTTL = ttl + } +} + +// WithCacheEnabled enables the cache. +func WithCacheEnabled() FetcherNextOption { + return func(o *fetcherNextOptions) { + o.useCache = true + } +} + +func WithAllowedSchemes(schemes ...string) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.schemes = schemes + } +} + +// WithHTTPClient will use the given HTTP client to fetch the JSON Web Keys. +func WithHTTPClient(c *retryablehttp.Client) FetcherNextOption { + return func(o *fetcherNextOptions) { + o.httpClient = c + } +} + +func (f *FetcherNext) ResolveKey(ctx context.Context, locations string, modifiers ...FetcherNextOption) (jwk.Key, error) { + return f.ResolveKeyFromLocations(ctx, []string{locations}, modifiers...) +} + +func (f *FetcherNext) ResolveKeyFromLocations(ctx context.Context, locations []string, modifiers ...FetcherNextOption) (jwk.Key, error) { + opts := new(fetcherNextOptions) + for _, m := range modifiers { + m(opts) + } + + if len(locations) > 1 && opts.forceKID == "" { + return nil, errors.Errorf("a key ID must be specified when multiple JWK sets are configured") + } + + set := jwk.NewSet() + eg := new(errgroup.Group) + for k := range locations { + location := locations[k] + eg.Go(func() error { + remoteSet, err := f.fetch(ctx, location, opts) + if err != nil { + return err + } + + iterator := remoteSet.Iterate(ctx) + for iterator.Next(ctx) { + // Pair().Value is always of type jwk.Key when generated by Iterate. + set.Add(iterator.Pair().Value.(jwk.Key)) + } + + return nil + }) + } + + if err := eg.Wait(); err != nil { + return nil, err + } + + if opts.forceKID != "" { + key, found := set.LookupKeyID(opts.forceKID) + if !found { + return nil, errors.WithStack(ErrUnableToFindKeyID) + } + + return key, nil + } + + // No KID was forced? Use the first key we can find. + key, found := set.Get(0) + if !found { + return nil, errors.WithStack(ErrUnableToFindKeyID) + } + + return key, nil +} + +// fetch fetches the JWK set from the given location and if enabled, may use the cache to look up the JWK set. +func (f *FetcherNext) fetch(ctx context.Context, location string, opts *fetcherNextOptions) (_ jwk.Set, err error) { + tracer := trace.SpanFromContext(ctx).TracerProvider().Tracer("") + ctx, span := tracer.Start(ctx, "jwksx.FetcherNext.fetch", trace.WithAttributes(attribute.String("location", location))) + defer otelx.End(span, &err) + + cacheKey := sha256.Sum256([]byte(location)) + if opts.useCache { + if result, found := f.cache.Get(cacheKey[:]); found { + return result, nil + } + } + + var fopts []fetcher.Modifier + if opts.httpClient != nil { + fopts = append(fopts, fetcher.WithClient(opts.httpClient)) + } + if len(opts.schemes) > 0 { + fopts = append(fopts, fetcher.WithAllowedSchemes(opts.schemes...)) + } + + result, err := fetcher.NewFetcher(fopts...).FetchContext(ctx, location) + if err != nil { + return nil, err + } + + set, err := jwk.ParseReader(result) + if err != nil { + return nil, errors.WithStack(herodot.ErrBadRequest().WithReason("failed to parse JWK set").WithWrap(err)) + } + + if opts.useCache { + f.cache.SetWithTTL(cacheKey[:], set, 1, opts.cacheTTL) + } + + return set, nil +} diff --git a/oryx/jwksx/generator.go b/oryx/jwksx/generator.go new file mode 100644 index 0000000..7fabb11 --- /dev/null +++ b/oryx/jwksx/generator.go @@ -0,0 +1,129 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwksx + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "io" + + "github.com/go-jose/go-jose/v3" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + "golang.org/x/crypto/ed25519" +) + +// GenerateSigningKeys generates a JSON Web Key Set for signing. +func GenerateSigningKeys(id, alg string, bits int) (*jose.JSONWebKeySet, error) { + if id == "" { + id = uuid.Must(uuid.NewV4()).String() + } + + key, err := generate(jose.SignatureAlgorithm(alg), bits) + if err != nil { + return nil, err + } + + return &jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Algorithm: alg, + Use: "sig", + Key: key, + KeyID: id, + Certificates: []*x509.Certificate{}, + }, + }, + }, nil +} + +// GenerateSigningKeysAvailableAlgorithms lists available algorithms that are supported by GenerateSigningKeys. +func GenerateSigningKeysAvailableAlgorithms() []string { + return []string{ + string(jose.HS256), string(jose.HS384), string(jose.HS512), + string(jose.ES256), string(jose.ES384), string(jose.ES512), string(jose.EdDSA), + string(jose.RS256), string(jose.RS384), string(jose.RS512), string(jose.PS256), string(jose.PS384), string(jose.PS512), + } +} + +// generate generates keypair for corresponding SignatureAlgorithm. +func generate(alg jose.SignatureAlgorithm, bits int) (crypto.PrivateKey, error) { + switch alg { + case jose.ES256, jose.ES384, jose.ES512, jose.EdDSA: + keylen := map[jose.SignatureAlgorithm]int{ + jose.ES256: 256, + jose.ES384: 384, + jose.ES512: 521, // sic! + jose.EdDSA: 256, + } + if bits != 0 && bits != keylen[alg] { + return nil, errors.Errorf(`jwksx: "%s" does not support arbitrary key length`, alg) + } + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + if bits == 0 { + bits = 2048 + } + if bits < 2048 { + return nil, errors.Errorf(`jwksx: key size must be at least 2048 bit for algorithm "%s"`, alg) + } + case jose.HS256: + if bits == 0 { + bits = 256 + } + if bits < 256 { + return nil, errors.Errorf(`jwksx: key size must be at least 256 bit for algorithm "%s"`, alg) + } + case jose.HS384: + if bits == 0 { + bits = 384 + } + if bits < 384 { + return nil, errors.Errorf(`jwksx: key size must be at least 2038448 bit for algorithm "%s"`, alg) + } + case jose.HS512: + if bits == 0 { + bits = 1024 + } + if bits < 512 { + return nil, errors.Errorf(`jwksx: key size must be at least 512 bit for algorithm "%s"`, alg) + } + } + + switch alg { + case jose.ES256: + // The cryptographic operations are implemented using constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.ES384: + // NB: The cryptographic operations do not use constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.ES512: + // NB: The cryptographic operations do not use constant-time algorithms. + key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.EdDSA: + _, key, err := ed25519.GenerateKey(rand.Reader) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.RS256, jose.RS384, jose.RS512, jose.PS256, jose.PS384, jose.PS512: + key, err := rsa.GenerateKey(rand.Reader, bits) + return key, errors.Wrapf(err, "jwks: unable to generate key") + case jose.HS256, jose.HS384, jose.HS512: + if bits%8 != 0 { + return nil, errors.Errorf(`jwksx: key size must be a multiple of 8 for algorithm "%s" but got: %d`, alg, bits) + } + + key := make([]byte, bits/8) + if _, err := io.ReadFull(rand.Reader, key); err != nil { + return nil, errors.Wrapf(err, "jwks: unable to generate key") + } + return key, nil + default: + return nil, errors.Errorf(`jwksx: available algorithms are "%+v" but unknown algorithm was requested: "%s"`, GenerateSigningKeysAvailableAlgorithms(), alg) + } +} diff --git a/oryx/jwtmiddleware/middleware.go b/oryx/jwtmiddleware/middleware.go new file mode 100644 index 0000000..4fd80d0 --- /dev/null +++ b/oryx/jwtmiddleware/middleware.go @@ -0,0 +1,155 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwtmiddleware + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + jwtmiddleware "github.com/auth0/go-jwt-middleware/v2" + "github.com/urfave/negroni" + + "github.com/ory/x/jwksx" +) + +type Middleware struct { + o *middlewareOptions + wku string + jm *jwtmiddleware.JWTMiddleware +} + +type middlewareOptions struct { + Debug bool + ExcludePaths []string + SigningMethod jwt.SigningMethod + ErrorWriter herodot.Writer +} + +type MiddlewareOption func(*middlewareOptions) + +func SessionFromContext(ctx context.Context) (json.RawMessage, error) { + raw := ctx.Value(jwtmiddleware.ContextKey{}) + if raw == nil { + return nil, errors.WithStack(herodot.ErrUnauthorized().WithReasonf("Could not find credentials in the request.")) + } + + token, ok := raw.(*jwt.Token) + if !ok { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithDebugf(`Expected context key "%T" to transport value of type *jwt.MapClaims but got type: %T`, jwtmiddleware.ContextKey{}, raw)) + } + + session, err := json.Marshal(token.Claims) + if err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError().WithDebugf("Unable to encode session data: %s", err)) + } + + return session, nil +} + +func MiddlewareDebugEnabled() MiddlewareOption { + return func(o *middlewareOptions) { + o.Debug = true + } +} + +func MiddlewareExcludePaths(paths ...string) MiddlewareOption { + return func(o *middlewareOptions) { + o.ExcludePaths = append(o.ExcludePaths, paths...) + } +} + +func MiddlewareAllowSigningMethod(method jwt.SigningMethod) MiddlewareOption { + return func(o *middlewareOptions) { + o.SigningMethod = method + } +} + +func MiddlewareErrorWriter(w herodot.Writer) MiddlewareOption { + return func(o *middlewareOptions) { + o.ErrorWriter = w + } +} + +func NewMiddleware( + wellKnownURL string, + opts ...MiddlewareOption, +) *Middleware { + c := &middlewareOptions{ + SigningMethod: jwt.SigningMethodES256, + ErrorWriter: herodot.NewJSONWriter(nil), + } + + for _, o := range opts { + o(c) + } + jc := jwksx.NewFetcher(wellKnownURL) + return &Middleware{ + o: c, + wku: wellKnownURL, + jm: jwtmiddleware.New( + func(ctx context.Context, rawToken string) (any, error) { + return jwt.NewParser( + jwt.WithValidMethods([]string{c.SigningMethod.Alg()}), + ).Parse(rawToken, func(token *jwt.Token) (interface{}, error) { + if raw, ok := token.Header["kid"]; !ok { + return nil, errors.New(`jwt from authorization HTTP header is missing value for "kid" in token header`) + } else if kid, ok := raw.(string); !ok { + return nil, fmt.Errorf(`jwt from authorization HTTP header is expecting string value for "kid" in tokenWithoutKid header but got: %T`, raw) + } else if k, err := jc.GetKey(kid); err != nil { + return nil, err + } else { + return k.Key, nil + } + }) + }, + jwtmiddleware.WithCredentialsOptional(false), + jwtmiddleware.WithTokenExtractor(func(r *http.Request) (string, error) { + // wrapping the extractor to get a herodot.ErrorContainer + token, err := jwtmiddleware.AuthHeaderTokenExtractor(r) + if err != nil { + return "", herodot.ErrUnauthorized().WithReason(err.Error()) + } + return token, nil + }), + jwtmiddleware.WithErrorHandler(func(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, jwtmiddleware.ErrJWTInvalid): + reason := "The token is invalid or expired." + if err := errors.Unwrap(err); err != nil { + reason = err.Error() + } + c.ErrorWriter.WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithReason(reason))) + case errors.Is(err, jwtmiddleware.ErrJWTMissing): + c.ErrorWriter.WriteError(w, r, errors.WithStack(herodot.ErrUnauthorized().WithReason("The token is missing."))) + default: + c.ErrorWriter.WriteError(w, r, err) + } + }), + ), + } +} + +// Deprecated: use Middleware as a negroni.Handler directly instead. +func (h *Middleware) NegroniHandler() negroni.Handler { + return negroni.HandlerFunc(h.ServeHTTP) +} + +func (h *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + for _, excluded := range h.o.ExcludePaths { + if strings.HasPrefix(r.URL.Path, excluded) { + next(w, r) + return + } + } + + h.jm.CheckJWT(next).ServeHTTP(w, r) +} diff --git a/oryx/jwtmiddleware/stub/jwks.json b/oryx/jwtmiddleware/stub/jwks.json new file mode 100644 index 0000000..57d130c --- /dev/null +++ b/oryx/jwtmiddleware/stub/jwks.json @@ -0,0 +1,10 @@ +{ + "use": "sig", + "kty": "EC", + "kid": "b71ff5bd-a016-4ac0-9f3f-a172552578ea", + "crv": "P-256", + "alg": "ES256", + "x": "7fVj_SeCx3TnkHANRWrpEho9BcYkU953LHUvKsSF5Wo", + "y": "2A9D_AAFPiJQLSJQ_h600Fy9jUrg9Q88gNPPZwHDb7o", + "d": "sRl-e-tGEVsNBF8FgEado9NAEipxhAFXGMryWDgbUMo" +} diff --git a/oryx/jwtx/claims.go b/oryx/jwtx/claims.go new file mode 100644 index 0000000..7bbbca9 --- /dev/null +++ b/oryx/jwtx/claims.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package jwtx + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/mapx" +) + +// Claims represents a JSON Web Token's standard claims. +type Claims struct { + // Audience identifies the recipients that the JWT is intended for. + Audience []string `json:"aud"` + + // Issuer identifies the principal that issued the JWT. + Issuer string `json:"iss"` + + // Subject identifies the principal that is the subject of the JWT. + Subject string `json:"sub"` + + // ExpiresAt identifies the expiration time on or after which the JWT most not be accepted for processing. + ExpiresAt time.Time `json:"exp"` + + // IssuedAt identifies the time at which the JWT was issued. + IssuedAt time.Time `json:"iat"` + + // NotBefore identifies the time before which the JWT must not be accepted for processing. + NotBefore time.Time `json:"nbf"` + + // JTI provides a unique identifier for the JWT. + JTI string `json:"jti"` +} + +// ParseMapStringInterfaceClaims converts map[string]interface{} to *Claims. +func ParseMapStringInterfaceClaims(claims map[string]interface{}) *Claims { + c := make(map[interface{}]interface{}) + for k, v := range claims { + c[k] = v + } + return ParseMapInterfaceInterfaceClaims(c) +} + +// ParseMapInterfaceInterfaceClaims converts map[interface{}]interface{} to *Claims. +func ParseMapInterfaceInterfaceClaims(claims map[interface{}]interface{}) *Claims { + result := &Claims{ + Issuer: mapx.GetStringDefault(claims, "iss", ""), + Subject: mapx.GetStringDefault(claims, "sub", ""), + JTI: mapx.GetStringDefault(claims, "jti", ""), + } + + if aud, err := mapx.GetString(claims, "aud"); err == nil { + result.Audience = []string{aud} + } else if errors.Is(err, mapx.ErrKeyCanNotBeTypeAsserted) { + if aud, err := mapx.GetStringSlice(claims, "aud"); err == nil { + result.Audience = aud + } else { + result.Audience = []string{} + } + } else { + result.Audience = []string{} + } + + if exp, err := mapx.GetTime(claims, "exp"); err == nil { + result.ExpiresAt = exp + } + + if iat, err := mapx.GetTime(claims, "iat"); err == nil { + result.IssuedAt = iat + } + + if nbf, err := mapx.GetTime(claims, "nbf"); err == nil { + result.NotBefore = nbf + } + + return result +} diff --git a/oryx/landlockx/landlockx.go b/oryx/landlockx/landlockx.go new file mode 100644 index 0000000..99f2e84 --- /dev/null +++ b/oryx/landlockx/landlockx.go @@ -0,0 +1,149 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package landlockx + +import ( + "os" + "path/filepath" + + ll "github.com/landlock-lsm/go-landlock/landlock" + llsyscall "github.com/landlock-lsm/go-landlock/landlock/syscall" + + "github.com/ory/x/logrusx" +) + +// IsSupported returns true if the running kernel supports Landlock LSM. +// On non-Linux platforms or kernels older than 5.13, this returns false. +func IsSupported() bool { + v, err := llsyscall.LandlockGetABIVersion() + return err == nil && v > 0 +} + +// runningExe returns the kernel-canonical path of the running binary. +// jsonnetsecure re-execs the running binary as a sandboxed worker, so +// the rule we attach must use the inode the kernel resolves at execve +// time. os.Executable on Linux returns the readlink of /proc/self/exe; +// EvalSymlinks resolves any further symlinks so the rule attaches to +// the actual file inode. +func runningExe() string { + exe, err := os.Executable() + if err != nil { + return "" + } + if real, err := filepath.EvalSymlinks(exe); err == nil { + return real + } + return exe +} + +// Apply activates the Landlock LSM filesystem sandbox for the calling process. +// +// roPaths is a list of paths that the process may read. Each path is stat-ed +// to determine whether to apply directory or file access rights; non-existent +// paths are silently skipped. +// +// rwDirs is a list of directories that the process may read and write +// recursively. Missing paths are silently ignored. +// +// Apply is a no-op on non-Linux platforms and on Linux kernels older than +// 5.13 (BestEffort degrades gracefully). +func Apply(l *logrusx.Logger, roPaths, rwDirs []string) error { + rules := defaultRules() + + for _, p := range roPaths { + info, err := os.Stat(p) + if err != nil { + continue + } + if info.IsDir() { + rules = append(rules, ll.RODirs(p)) + } else { + rules = append(rules, ll.ROFiles(p)) + } + } + + for _, d := range rwDirs { + rules = append(rules, ll.RWDirs(d).IgnoreIfMissing()) + } + + if !IsSupported() { + l.Warn("Landlock LSM is not available on this kernel; filesystem sandbox not applied.") + return nil + } + + if err := ll.V3.BestEffort().RestrictPaths(rules...); err != nil { + return err + } + + l.Info("Landlock filesystem sandbox is active.") + return nil +} + +// ApplyEmpty activates a Landlock filesystem sandbox with no allowed +// paths, denying every filesystem access the V3 ABI handles. Already-open +// file descriptors (stdin, stdout, stderr inherited from the parent) +// keep working, but no path-based open/read/write/exec is permitted. +// +// Use it inside helper subprocesses that only need stdio — for example, +// the hidden `jsonnet` worker that jsonnetsecure re-execs to evaluate +// mappers in isolation. +// +// Landlock layers stack: this call adds a strictly-more-restrictive +// layer on top of whatever the parent already enforced. +// +// ApplyEmpty is a no-op on non-Linux platforms and on Linux kernels +// older than 5.13. +func ApplyEmpty(l *logrusx.Logger) error { + if !IsSupported() { + if l != nil { + l.Warn("Landlock LSM is not available on this kernel; empty sandbox not applied.") + } + return nil + } + if err := ll.V3.BestEffort().RestrictPaths(); err != nil { + return err + } + if l != nil { + l.Info("Landlock empty filesystem sandbox is active.") + } + return nil +} + +// defaultRules returns the rules every kratos serve process needs regardless +// of operator config: +// - /dev/null is a kernel-managed sink used by subprocess plumbing +// (exec.Command stdin redirection, log discarding, etc.); it cannot +// leak data. +// - /etc/resolv.conf carries the DNS nameserver list and /etc/hosts +// the static host table; Go's pure-Go resolver reads both for every +// hostname lookup. /etc/nsswitch.conf is intentionally not granted: +// the binary is built with CGO_ENABLED=0, so when nsswitch.conf is +// unreadable the resolver falls back to "files then DNS", which is +// what we want anyway. +// - the running binary is re-exec'd by jsonnetsecure for sandboxed mapper +// evaluation, so it needs read+execute. The binary must be statically +// linked (CGO_ENABLED=0) — execve of a dynamically-linked binary also +// requires read+execute on the system dynamic linker (PT_INTERP) and +// its NEEDED libraries, which we deliberately do not grant. +// +// /etc/ssl (system trust roots for outbound HTTPS) is intentionally not +// included. Cloud binaries blank-import +// golang.org/x/crypto/x509roots/fallback (see cloudlib/x509_roots.go) +// and set `godebug x509usefallbackroots=1` in their go.mod, so +// crypto/x509 ships its own embedded Mozilla bundle and never reads +// /etc/ssl at runtime. Operators who need to trust an additional CA +// continue to set SSL_CERT_FILE / SSL_CERT_DIR and list the file under +// security.landlock.allowed_paths. +func defaultRules() []ll.Rule { + rules := []ll.Rule{ + ll.RWFiles("/dev/null"), + ll.ROFiles("/etc/resolv.conf").IgnoreIfMissing(), + ll.ROFiles("/etc/hosts").IgnoreIfMissing(), + } + if exe := runningExe(); exe != "" { + access := ll.AccessFSSet(llsyscall.AccessFSReadFile | llsyscall.AccessFSExecute) + rules = append(rules, ll.PathAccess(access, exe).IgnoreIfMissing()) + } + return rules +} diff --git a/oryx/landlockx/paths.go b/oryx/landlockx/paths.go new file mode 100644 index 0000000..1ccfd4c --- /dev/null +++ b/oryx/landlockx/paths.go @@ -0,0 +1,32 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package landlockx + +import ( + "path/filepath" + "strings" + + "github.com/ory/x/sqlxx" +) + +// SQLiteDirFromDSN returns the directory containing the SQLite database +// referenced by the DSN. Granting the directory once is simpler and more +// robust than enumerating every sibling: SQLite manages -journal in +// rollback mode and -wal/-shm in WAL mode, plus transient -mj-XXXXX +// rollback masters during multi-database checkpoints — the latter use +// random suffixes that a per-file rule cannot cover. +// +// Returns an empty string for non-SQLite DSNs and for the in-memory +// SQLite DSN. +func SQLiteDirFromDSN(dsn string) string { + scheme, path, err := sqlxx.ExtractSchemeFromDSN(dsn) + if err != nil || (scheme != "sqlite" && scheme != "sqlite3") { + return "" + } + path, _, _ = strings.Cut(path, "?") + if path == "" || strings.Contains(path, ":memory:") { + return "" + } + return filepath.Dir(path) +} diff --git a/oryx/logrusx/config.schema.json b/oryx/logrusx/config.schema.json new file mode 100644 index 0000000..568ea90 --- /dev/null +++ b/oryx/logrusx/config.schema.json @@ -0,0 +1,43 @@ +{ + "$id": "ory://logging-config", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Log", + "description": "Configure logging using the following options. Logs will always be sent to stdout and stderr.", + "type": "object", + "properties": { + "level": { + "title": "Level", + "description": "The level of log entries to show. Debug enables stack traces on errors.", + "type": "string", + "default": "info", + "enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"] + }, + "format": { + "title": "Log Format", + "description": "The output format of log messages.", + "type": "string", + "default": "text", + "enum": ["json", "json_pretty", "gelf", "text"] + }, + "leak_sensitive_values": { + "type": "boolean", + "title": "Leak Sensitive Log Values", + "description": "If set will leak sensitive values (e.g. emails) in the logs.", + "default": false + }, + "redaction_text": { + "type": "string", + "title": "Sensitive log value redaction text", + "description": "Text to use, when redacting sensitive log value." + }, + "additional_redacted_headers": { + "type": "array", + "title": "Additional redacted headers", + "description": "List of HTTP headers which will be redacted.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false +} diff --git a/oryx/logrusx/helper.go b/oryx/logrusx/helper.go new file mode 100644 index 0000000..551346a --- /dev/null +++ b/oryx/logrusx/helper.go @@ -0,0 +1,293 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logrusx + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "reflect" + "strings" + "sync" + + "github.com/sirupsen/logrus" + + "go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/errorsx" +) + +// loggerConfig holds the mutable state that can change after construction via +// UseConfig. It is accessed via a pointer so all shallow copies of Logger +// (WithField, WithFields, WithContext, NewEntry) share and see the same config. +type loggerConfig struct { + mu sync.RWMutex + leakSensitive bool + redactionText string + additionalRedactedHeaders map[string]struct{} +} + +type ( + Logger struct { + *logrus.Entry + cfg *loggerConfig + opts []Option + } + Provider interface { + Logger() *Logger + } +) + +var opts = otelhttptrace.WithPropagators(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + +func (l *Logger) Logrus() *logrus.Logger { + return l.Entry.Logger +} + +func (l *Logger) NewEntry() *Logger { + ll := *l + ll.Entry = logrus.NewEntry(l.Logger) + return &ll +} + +func (l *Logger) WithContext(ctx context.Context) *Logger { + ll := *l + ll.Entry = l.Logger.WithContext(ctx) + return &ll +} + +func (l *Logger) HTTPHeadersRedacted(h http.Header) map[string]any { + // Snapshot the mutable config fields once under the read lock so we neither + // hold the lock during iteration nor risk a concurrent map write via UseConfig. + l.cfg.mu.RLock() + leakSensitive := l.cfg.leakSensitive + redactionText := l.cfg.redactionText + additionalHeaders := l.cfg.additionalRedactedHeaders + l.cfg.mu.RUnlock() + + headers := map[string]any{} + + for key, value := range h { + switch keyLower := strings.ToLower(key); keyLower { + case "authorization", "cookie", "set-cookie", "x-session-token": + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + case "location": + locationURL, err := url.Parse(h.Get("Location")) + if err != nil { + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + continue + } + if leakSensitive { + headers[keyLower] = locationURL.String() + } else { + locationURL.RawQuery = "" + locationURL.Fragment = "" + headers[keyLower] = locationURL.Redacted() + } + default: + if _, ok := additionalHeaders[keyLower]; ok { + headers[keyLower] = maybeRedactWith(value, leakSensitive, redactionText) + continue + } + headers[keyLower] = h.Get(key) + } + } + + return headers +} + +func (l *Logger) WithRequest(r *http.Request) *Logger { + headers := l.HTTPHeadersRedacted(r.Header) + if ua := r.UserAgent(); len(ua) > 0 { + headers["user-agent"] = ua + } + + scheme := "https" + if r.TLS == nil { + scheme = "http" + } + + ll := l.WithField("http_request", map[string]any{ + "remote": r.RemoteAddr, + "method": r.Method, + "path": r.URL.EscapedPath(), + "query": l.maybeRedact(r.URL.RawQuery), + "scheme": scheme, + "host": r.Host, + "headers": headers, + }) + + spanCtx := trace.SpanContextFromContext(r.Context()) + if !spanCtx.IsValid() { + _, _, spanCtx = otelhttptrace.Extract(r.Context(), r, opts) + } + if spanCtx.IsValid() { + traces := make(map[string]string, 2) + if spanCtx.HasTraceID() { + traces["trace_id"] = spanCtx.TraceID().String() + } + if spanCtx.HasSpanID() { + traces["span_id"] = spanCtx.SpanID().String() + } + ll = ll.WithField("otel", traces) + } + return ll +} + +func (l *Logger) WithSpanFromContext(ctx context.Context) *Logger { + spanCtx := trace.SpanContextFromContext(ctx) + if !spanCtx.IsValid() { + return l + } + + traces := make(map[string]string, 2) + if spanCtx.HasTraceID() { + traces["trace_id"] = spanCtx.TraceID().String() + } + if spanCtx.HasSpanID() { + traces["span_id"] = spanCtx.SpanID().String() + } + return l.WithField("otel", traces) +} + +func (l *Logger) Logf(level logrus.Level, format string, args ...any) { + l.cfg.mu.RLock() + leakSensitive := l.cfg.leakSensitive + l.cfg.mu.RUnlock() + + if !leakSensitive { + for i, arg := range args { + switch urlArg := arg.(type) { + case url.URL: + urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path} + args[i] = urlCopy + case *url.URL: + urlCopy := url.URL{Scheme: urlArg.Scheme, Host: urlArg.Host, Path: urlArg.Path} + args[i] = &urlCopy + default: + continue + } + } + } + l.Entry.Logf(level, format, args...) +} + +func (l *Logger) Tracef(format string, args ...any) { + l.Logf(logrus.TraceLevel, format, args...) +} + +func (l *Logger) Debugf(format string, args ...any) { + l.Logf(logrus.DebugLevel, format, args...) +} + +func (l *Logger) Infof(format string, args ...any) { + l.Logf(logrus.InfoLevel, format, args...) +} + +func (l *Logger) Warnf(format string, args ...any) { + l.Logf(logrus.WarnLevel, format, args...) +} + +func (l *Logger) Errorf(format string, args ...any) { + l.Logf(logrus.ErrorLevel, format, args...) +} + +func (l *Logger) Fatalf(format string, args ...any) { + l.Logf(logrus.FatalLevel, format, args...) + l.Entry.Logger.Exit(1) +} + +func (l *Logger) Panicf(format string, args ...any) { + l.Logf(logrus.PanicLevel, format, args...) +} + +func (l *Logger) WithFields(f logrus.Fields) *Logger { + ll := *l + ll.Entry = l.Entry.WithFields(f) + return &ll +} + +func (l *Logger) WithField(key string, value any) *Logger { + ll := *l + ll.Entry = l.Entry.WithField(key, value) + return &ll +} + +// maybeRedactWith is the lock-free redaction helper; callers must supply +// pre-snapshotted values of leakSensitive and redactionText. +func maybeRedactWith(value any, leakSensitive bool, redactionText string) any { + if fmt.Sprintf("%v", value) == "" || value == nil { + return nil + } + if !leakSensitive { + return redactionText + } + return value +} + +func (l *Logger) maybeRedact(value any) any { + l.cfg.mu.RLock() + leak := l.cfg.leakSensitive + text := l.cfg.redactionText + l.cfg.mu.RUnlock() + return maybeRedactWith(value, leak, text) +} + +func (l *Logger) WithSensitiveField(key string, value any) *Logger { + return l.WithField(key, l.maybeRedact(value)) +} + +func (l *Logger) WithError(err error) *Logger { + if err == nil { + return l + } + + ctx := map[string]any{"message": err.Error()} + if l.Entry.Logger.IsLevelEnabled(logrus.DebugLevel) { + if e, ok := err.(errorsx.StackTracer); ok { + ctx["stack_trace"] = fmt.Sprintf("%+v", e.StackTrace()) + } else { + ctx["stack_trace"] = fmt.Sprintf("stack trace could not be recovered from error type %s", reflect.TypeOf(err)) + } + } + if c := errorsx.ReasonCarrier(nil); errors.As(err, &c) { + ctx["reason"] = c.Reason() + } + if c := errorsx.RequestIDCarrier(nil); errors.As(err, &c) && c.RequestID() != "" { + ctx["request_id"] = c.RequestID() + } + if c := errorsx.DetailsCarrier(nil); errors.As(err, &c) && c.Details() != nil { + ctx["details"] = c.Details() + } + if c := errorsx.StatusCarrier(nil); errors.As(err, &c) && c.Status() != "" { + ctx["status"] = c.Status() + } + if c := errorsx.StatusCodeCarrier(nil); errors.As(err, &c) && c.StatusCode() != 0 { + ctx["status_code"] = c.StatusCode() + } + if c := errorsx.DebugCarrier(nil); errors.As(err, &c) { + ctx["debug"] = c.Debug() + } + + return l.WithField("error", ctx) +} + +func (l *Logger) StdLogger(lvl logrus.Level) *log.Logger { + return log.New(writer{l.Logger, lvl}, "", 0) +} + +type writer struct { + l *logrus.Logger + lvl logrus.Level +} + +func (w writer) Write(p []byte) (n int, err error) { + w.l.Log(w.lvl, strings.TrimSuffix(string(p), "\n")) + return len(p), nil +} diff --git a/oryx/logrusx/logrus.go b/oryx/logrusx/logrus.go new file mode 100644 index 0000000..eff9a29 --- /dev/null +++ b/oryx/logrusx/logrus.go @@ -0,0 +1,278 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package logrusx + +import ( + "bytes" + "cmp" + _ "embed" + "io" + "maps" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/sirupsen/logrus" + + gelf "github.com/seatgeek/logrus-gelf-formatter" + + "github.com/ory/x/stringsx" +) + +type ( + options struct { + l *logrus.Logger + level *logrus.Level + formatter logrus.Formatter + format string + reportCaller bool + exitFunc func(int) + leakSensitive bool + redactionText string + additionalRedactedHeaders []string + hooks []logrus.Hook + c configurator + } + Option func(*options) + nullConfigurator struct{} + configurator interface { + Bool(key string) bool + String(key string) string + Strings(path string) []string + } +) + +//go:embed config.schema.json +var ConfigSchema string + +const ConfigSchemaID = "ory://logging-config" + +// AddConfigSchema adds the logging schema to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddConfigSchema(c interface { + AddResource(url string, r io.Reader) error +}, +) error { + return c.AddResource(ConfigSchemaID, bytes.NewBufferString(ConfigSchema)) +} + +func newLogger(parent *logrus.Logger, o *options) *logrus.Logger { + l := parent + if l == nil { + l = logrus.New() + } + + if o.exitFunc != nil { + l.ExitFunc = o.exitFunc + } + + for _, hook := range o.hooks { + l.AddHook(hook) + } + + setLevel(l, o) + setFormatter(l, o) + + l.ReportCaller = o.reportCaller || l.IsLevelEnabled(logrus.TraceLevel) + return l +} + +func setLevel(l *logrus.Logger, o *options) { + if o.level != nil { + l.SetLevel(*o.level) + } else { + level, err := logrus.ParseLevel(cmp.Or( + o.c.String("log.level"), + os.Getenv("LOG_LEVEL"))) + if err != nil { + level = logrus.InfoLevel + } + l.SetLevel(level) + } +} + +func setFormatter(l *logrus.Logger, o *options) { + if o.formatter != nil { + l.SetFormatter(o.formatter) + } else { + var unknownFormat bool // we first have to set the formatter before we can complain about the unknown format + var formatter logrus.Formatter + + format := stringsx.SwitchExact(cmp.Or(o.format, o.c.String("log.format"), os.Getenv("LOG_FORMAT"))) + switch { + case format.AddCase("json"): + formatter = &logrus.JSONFormatter{PrettyPrint: false, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true} + case format.AddCase("json_pretty"): + formatter = &logrus.JSONFormatter{PrettyPrint: true, TimestampFormat: time.RFC3339Nano, DisableHTMLEscape: true} + case format.AddCase("gelf"): + formatter = new(gelf.GelfFormatter) + default: + unknownFormat = true + fallthrough + case format.AddCase("text", ""): + formatter = &logrus.TextFormatter{ + DisableQuote: true, + DisableTimestamp: false, + FullTimestamp: true, + } + } + + l.SetFormatter(formatter) + + if unknownFormat { + l.WithError(format.ToUnknownCaseErr()).Warn("got unknown \"log.format\", falling back to \"text\"") + } + } +} + +func ForceLevel(level logrus.Level) Option { + return func(o *options) { + o.level = &level + } +} + +func ForceFormatter(formatter logrus.Formatter) Option { + return func(o *options) { + o.formatter = formatter + } +} + +func WithConfigurator(c configurator) Option { + return func(o *options) { + o.c = c + } +} + +func ForceFormat(format string) Option { + return func(o *options) { + o.format = format + } +} + +func WithHook(hook logrus.Hook) Option { + return func(o *options) { + o.hooks = append(o.hooks, hook) + } +} + +func WithExitFunc(exitFunc func(int)) Option { + return func(o *options) { + o.exitFunc = exitFunc + } +} + +func ReportCaller(reportCaller bool) Option { + return func(o *options) { + o.reportCaller = reportCaller + } +} + +func UseLogger(l *logrus.Logger) Option { + return func(o *options) { + o.l = l + } +} + +func LeakSensitive() Option { + return func(o *options) { + o.leakSensitive = true + } +} + +func RedactionText(text string) Option { + return func(o *options) { + o.redactionText = text + } +} + +func WithAdditionalRedactedHeaders(headers []string) Option { + return func(o *options) { + o.additionalRedactedHeaders = headers + } +} + +func toHeaderMap(headers []string) map[string]struct{} { + m := make(map[string]struct{}, len(headers)) + for _, h := range headers { + m[strings.ToLower(h)] = struct{}{} + } + return m +} + +func (c *nullConfigurator) Bool(_ string) bool { return false } +func (c *nullConfigurator) String(_ string) string { return "" } +func (c *nullConfigurator) Strings(_ string) []string { return []string{} } + +func newOptions(opts []Option) *options { + o := new(options) + o.c = new(nullConfigurator) + for _, f := range opts { + f(o) + } + return o +} + +// New creates a new logger with all the important fields set. +func New(name string, version string, opts ...Option) *Logger { + o := newOptions(opts) + return &Logger{ + opts: opts, + cfg: &loggerConfig{ + leakSensitive: o.leakSensitive || o.c.Bool("log.leak_sensitive_values"), + redactionText: cmp.Or(o.redactionText, `Value is sensitive and has been redacted. To see the value set config key "log.leak_sensitive_values = true" or environment variable "LOG_LEAK_SENSITIVE_VALUES=true".`), + additionalRedactedHeaders: toHeaderMap(func() []string { + if len(o.additionalRedactedHeaders) > 0 { + return o.additionalRedactedHeaders + } + return o.c.Strings("log.additional_redacted_headers") + }()), + }, + Entry: newLogger(o.l, o).WithFields(logrus.Fields{ + "audience": "application", "service_name": name, "service_version": version, + }), + } +} + +func NewT(t testing.TB, opts ...Option) *Logger { + opts = append(opts, LeakSensitive(), WithExitFunc(func(code int) { + t.Fatalf("Logger exited with code %d", code) + })) + l := New(t.Name(), "test", opts...) + l.Logger.SetOutput(t.Output()) + return l +} + +func (l *Logger) UseConfig(c configurator) { + // Update logrus level and formatter first, using their mutex-safe methods, + // before acquiring our own lock to avoid any lock ordering issues. + o := newOptions(append(l.opts, WithConfigurator(c))) + setLevel(l.Entry.Logger, o) + setFormatter(l.Entry.Logger, o) + + l.cfg.mu.Lock() + defer l.cfg.mu.Unlock() + l.cfg.leakSensitive = l.cfg.leakSensitive || c.Bool("log.leak_sensitive_values") + l.cfg.redactionText = cmp.Or(c.String("log.redaction_text"), l.cfg.redactionText) + // Replace the map rather than mutating it in-place so that readers holding + // a snapshot of the old pointer can iterate it safely without a lock. + newHeaders := maps.Clone(l.cfg.additionalRedactedHeaders) + for k := range toHeaderMap(c.Strings("log.additional_redacted_headers")) { + newHeaders[k] = struct{}{} + } + l.cfg.additionalRedactedHeaders = newHeaders +} + +func (l *Logger) ReportError(r *http.Request, code int, err error, args ...interface{}) { + logger := l.WithError(err).WithRequest(r).WithField("http_response", map[string]interface{}{ + "status_code": code, + }) + switch { + case code < 500: + logger.Info(args...) + default: + logger.Error(args...) + } +} diff --git a/oryx/mailhog/memory.go b/oryx/mailhog/memory.go new file mode 100644 index 0000000..5ee5ce7 --- /dev/null +++ b/oryx/mailhog/memory.go @@ -0,0 +1,212 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package mailhog is a copy of github.com/mailhog/storage with the missing +// read mutex operations added to List, Count, Search, and Load. +package mailhog + +import ( + "errors" + "strings" + "sync" + + "github.com/mailhog/data" +) + +// InMemory is an in memory storage backend +type InMemory struct { + MessageIDIndex map[string]int + Messages []*data.Message + mu sync.RWMutex +} + +// NewInMemory creates a new in memory storage backend +func NewInMemory() *InMemory { + return &InMemory{ + MessageIDIndex: make(map[string]int), + Messages: make([]*data.Message, 0), + } +} + +// Store stores a message and returns its storage ID +func (memory *InMemory) Store(m *data.Message) (string, error) { + memory.mu.Lock() + defer memory.mu.Unlock() + memory.Messages = append(memory.Messages, m) + memory.MessageIDIndex[string(m.ID)] = len(memory.Messages) - 1 + return string(m.ID), nil +} + +// Count returns the number of stored messages +func (memory *InMemory) Count() int { + memory.mu.RLock() + defer memory.mu.RUnlock() + return len(memory.Messages) +} + +// Search finds messages matching the query +func (memory *InMemory) Search(kind, query string, start, limit int) (*data.Messages, int, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + // FIXME needs optimising, or replacing with a proper db! + query = strings.ToLower(query) + var filteredMessages = make([]*data.Message, 0) + for _, m := range memory.Messages { + doAppend := false + + switch kind { + case "to": + for _, to := range m.To { + if strings.Contains(strings.ToLower(to.Mailbox+"@"+to.Domain), query) { + doAppend = true + break + } + } + if !doAppend { + if hdr, ok := m.Content.Headers["To"]; ok { + for _, to := range hdr { + if strings.Contains(strings.ToLower(to), query) { + doAppend = true + break + } + } + } + } + case "from": + if strings.Contains(strings.ToLower(m.From.Mailbox+"@"+m.From.Domain), query) { + doAppend = true + } + if !doAppend { + if hdr, ok := m.Content.Headers["From"]; ok { + for _, from := range hdr { + if strings.Contains(strings.ToLower(from), query) { + doAppend = true + break + } + } + } + } + case "containing": + if strings.Contains(strings.ToLower(m.Content.Body), query) { + doAppend = true + } + if !doAppend { + for _, hdr := range m.Content.Headers { + for _, v := range hdr { + if strings.Contains(strings.ToLower(v), query) { + doAppend = true + } + } + } + } + } + + if doAppend { + filteredMessages = append(filteredMessages, m) + } + } + + var messages = make([]data.Message, 0) + + if len(filteredMessages) == 0 || start > len(filteredMessages) { + msgs := data.Messages(messages) + return &msgs, 0, nil + } + + if start+limit > len(filteredMessages) { + limit = len(filteredMessages) - start + } + + start = len(filteredMessages) - start - 1 + end := start - limit + + if start < 0 { + start = 0 + } + if end < -1 { + end = -1 + } + + for i := start; i > end; i-- { + //for _, m := range memory.MessageIndex[start:end] { + messages = append(messages, *filteredMessages[i]) + } + + msgs := data.Messages(messages) + return &msgs, len(filteredMessages), nil +} + +// List lists stored messages by index +func (memory *InMemory) List(start int, limit int) (*data.Messages, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + var messages = make([]data.Message, 0) + + if len(memory.Messages) == 0 || start > len(memory.Messages) { + msgs := data.Messages(messages) + return &msgs, nil + } + + if start+limit > len(memory.Messages) { + limit = len(memory.Messages) - start + } + + start = len(memory.Messages) - start - 1 + end := start - limit + + if start < 0 { + start = 0 + } + if end < -1 { + end = -1 + } + + for i := start; i > end; i-- { + //for _, m := range memory.MessageIndex[start:end] { + messages = append(messages, *memory.Messages[i]) + } + + msgs := data.Messages(messages) + return &msgs, nil +} + +// DeleteOne deletes an individual message by storage ID +func (memory *InMemory) DeleteOne(id string) error { + memory.mu.Lock() + defer memory.mu.Unlock() + + var index int + var ok bool + + if index, ok = memory.MessageIDIndex[id]; !ok && true { + return errors.New("message not found") + } + + delete(memory.MessageIDIndex, id) + for k, v := range memory.MessageIDIndex { + if v > index { + memory.MessageIDIndex[k] = v - 1 + } + } + memory.Messages = append(memory.Messages[:index], memory.Messages[index+1:]...) + return nil +} + +// DeleteAll deletes all in memory messages +func (memory *InMemory) DeleteAll() error { + memory.mu.Lock() + defer memory.mu.Unlock() + memory.Messages = make([]*data.Message, 0) + memory.MessageIDIndex = make(map[string]int) + return nil +} + +// Load returns an individual message by storage ID +func (memory *InMemory) Load(id string) (*data.Message, error) { + memory.mu.RLock() + defer memory.mu.RUnlock() + if idx, ok := memory.MessageIDIndex[id]; ok { + return memory.Messages[idx], nil + } + return nil, nil +} diff --git a/oryx/mapx/type_assert.go b/oryx/mapx/type_assert.go new file mode 100644 index 0000000..39a5b6b --- /dev/null +++ b/oryx/mapx/type_assert.go @@ -0,0 +1,180 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package mapx + +import ( + "encoding/json" + "errors" + "math" + "time" +) + +// ErrKeyDoesNotExist is returned when the key does not exist in the map. +var ErrKeyDoesNotExist = errors.New("key is not present in map") + +// ErrKeyCanNotBeTypeAsserted is returned when the key can not be type asserted. +var ErrKeyCanNotBeTypeAsserted = errors.New("key could not be type asserted") + +// GetString returns a string for a given key in values. +func GetString[K comparable](values map[K]any, key K) (string, error) { + if v, ok := values[key]; !ok { + return "", ErrKeyDoesNotExist + } else if sv, ok := v.(string); !ok { + return "", ErrKeyCanNotBeTypeAsserted + } else { + return sv, nil + } +} + +// GetStringSlice returns a string slice for a given key in values. +func GetStringSlice[K comparable](values map[K]any, key K) ([]string, error) { + v, ok := values[key] + if !ok { + return nil, ErrKeyDoesNotExist + } + switch v := v.(type) { + case []string: + return v, nil + case []any: + vs := make([]string, len(v)) + for k, v := range v { + var ok bool + vs[k], ok = v.(string) + if !ok { + return nil, ErrKeyCanNotBeTypeAsserted + } + } + return vs, nil + } + return nil, ErrKeyCanNotBeTypeAsserted +} + +// GetTime returns a string slice for a given key in values. +func GetTime[K comparable](values map[K]any, key K) (time.Time, error) { + v, ok := values[key] + if !ok { + return time.Time{}, ErrKeyDoesNotExist + } + + switch v := v.(type) { + case time.Time: + return v, nil + case int64: + return time.Unix(v, 0), nil + case int32: + return time.Unix(int64(v), 0), nil + case int: + return time.Unix(int64(v), 0), nil + case float64: + if v < math.MinInt64 || v > math.MaxInt64 { + return time.Time{}, errors.New("value is out of range") + } + return time.Unix(int64(v), 0), nil + case float32: + if v < math.MinInt64 || v > math.MaxInt64 { + return time.Time{}, errors.New("value is out of range") + } + return time.Unix(int64(v), 0), nil + } + + return time.Time{}, ErrKeyCanNotBeTypeAsserted +} + +// GetInt64 returns an int64 for a given key in values. +func GetInt64[K comparable](values map[K]any, key K) (int64, error) { + v, ok := values[key] + if !ok { + return 0, ErrKeyDoesNotExist + } + switch v := v.(type) { + case json.Number: + return v.Int64() + case int64: + return v, nil + case int: + return int64(v), nil + case int32: + return int64(v), nil + case uint: + vv := uint64(v) + if vv > math.MaxInt64 { + return 0, errors.New("value is out of range") + } + return int64(vv), nil + case uint32: + return int64(v), nil + case uint64: + if v > math.MaxInt64 { + return 0, errors.New("value is out of range") + } + return int64(v), nil + } + return 0, ErrKeyCanNotBeTypeAsserted +} + +// GetInt32 returns an int32 for a given key in values. +func GetInt32[K comparable](values map[K]any, key K) (int32, error) { + v, err := GetInt64(values, key) + if err != nil { + return 0, err + } + if v > math.MaxInt32 || v < math.MinInt32 { + return 0, errors.New("value is out of range") + } + return int32(v), nil +} + +// GetInt returns an int for a given key in values. +func GetInt[K comparable](values map[K]any, key K) (int, error) { + v, err := GetInt64(values, key) + if err != nil { + return 0, err + } + if v > math.MaxInt || v < math.MinInt { + return 0, errors.New("value is out of range") + } + return int(v), nil +} + +// GetFloat64Default returns a float64 or the default value for a given key in values. +func GetFloat64Default[K comparable](values map[K]any, key K, defaultValue float64) float64 { + f, err := GetFloat64(values, key) + if err != nil { + return defaultValue + } + return f +} + +// GetFloat64 returns a float64 for a given key in values. +func GetFloat64[K comparable](values map[K]any, key K) (float64, error) { + v, ok := values[key] + if !ok { + return 0, ErrKeyDoesNotExist + } + switch v := v.(type) { + case json.Number: + return v.Float64() + case float32: + return float64(v), nil + case float64: + return v, nil + } + return 0, ErrKeyCanNotBeTypeAsserted +} + +// GetStringDefault returns a string or the default value for a given key in values. +func GetStringDefault[K comparable](values map[K]any, key K, defaultValue string) string { + if s, err := GetString(values, key); err == nil { + return s + } + return defaultValue +} + +// GetStringSliceDefault returns a string slice or the default value for a given key in values. +func GetStringSliceDefault[K comparable](values map[K]any, key K, defaultValue []string) []string { + if s, err := GetStringSlice(values, key); err == nil { + return s + } + return defaultValue +} diff --git a/oryx/metricsx/metrics.go b/oryx/metricsx/metrics.go new file mode 100644 index 0000000..25aab63 --- /dev/null +++ b/oryx/metricsx/metrics.go @@ -0,0 +1,84 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package metricsx + +import ( + "runtime" + "sync" +) + +// MemoryStatistics is a JSON-able version of runtime.MemStats +type MemoryStatistics struct { + sync.Mutex + // Alloc is bytes of allocated heap objects. + Alloc uint64 `json:"alloc"` + // TotalAlloc is cumulative bytes allocated for heap objects. + TotalAlloc uint64 `json:"totalAlloc"` + // Sys is the total bytes of memory obtained from the OS. + Sys uint64 `json:"sys"` + // Lookups is the number of pointer lookups performed by the + // runtime. + Lookups uint64 `json:"lookups"` + // Mallocs is the cumulative count of heap objects allocated. + // The number of live objects is Mallocs - Frees. + Mallocs uint64 `json:"mallocs"` + // Frees is the cumulative count of heap objects freed. + Frees uint64 `json:"frees"` + // HeapAlloc is bytes of allocated heap objects. + HeapAlloc uint64 `json:"heapAlloc"` + // HeapSys is bytes of heap memory obtained from the OS. + HeapSys uint64 `json:"heapSys"` + // HeapIdle is bytes in idle (unused) spans. + HeapIdle uint64 `json:"heapIdle"` + // HeapInuse is bytes in in-use spans. + HeapInuse uint64 `json:"heapInuse"` + // HeapReleased is bytes of physical memory returned to the OS. + HeapReleased uint64 `json:"heapReleased"` + // HeapObjects is the number of allocated heap objects. + HeapObjects uint64 `json:"heapObjects"` + // NumGC is the number of completed GC cycles. + NumGC uint32 `json:"numGC"` +} + +// ToMap converts to a map[string]interface{}. +func (ms *MemoryStatistics) ToMap() map[string]interface{} { + return map[string]interface{}{ + "alloc": ms.Alloc, + "totalAlloc": ms.TotalAlloc, + "sys": ms.Sys, + "lookups": ms.Lookups, + "mallocs": ms.Mallocs, + "frees": ms.Frees, + "heapAlloc": ms.HeapAlloc, + "heapSys": ms.HeapSys, + "heapIdle": ms.HeapIdle, + "heapInuse": ms.HeapInuse, + "heapReleased": ms.HeapReleased, + "heapObjects": ms.HeapObjects, + "numGC": ms.NumGC, + "nonInteraction": 1, + } +} + +// Update takes the most recent stats from runtime. +func (ms *MemoryStatistics) Update() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + + ms.Lock() + defer ms.Unlock() + ms.Alloc = m.Alloc + ms.TotalAlloc = m.TotalAlloc + ms.Sys = m.Sys + ms.Lookups = m.Lookups + ms.Mallocs = m.Mallocs + ms.Frees = m.Frees + ms.HeapAlloc = m.HeapAlloc + ms.HeapSys = m.HeapSys + ms.HeapIdle = m.HeapIdle + ms.HeapInuse = m.HeapInuse + ms.HeapReleased = m.HeapReleased + ms.HeapObjects = m.HeapObjects + ms.NumGC = m.NumGC +} diff --git a/oryx/metricsx/middleware.go b/oryx/metricsx/middleware.go new file mode 100644 index 0000000..0205382 --- /dev/null +++ b/oryx/metricsx/middleware.go @@ -0,0 +1,427 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package metricsx + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "math" + "net/http" + "net/url" + "os" + "runtime" + "strconv" + "strings" + "sync" + "time" + + "connectrpc.com/connect" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + + "github.com/gofrs/uuid" + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/configx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/resilience" + "github.com/ory/x/urlx" + + "github.com/ory/analytics-go/v5" +) + +const ( + XForwardedHostHeader = "X-Forwarded-Host" + AuthorityHeader = ":authority" +) + +var ( + instance *Service + lock sync.Mutex + knownHeaders = []string{AuthorityHeader, XForwardedHostHeader} +) + +// Service helps with providing context on metrics. +type Service struct { + optOut bool + instanceId string + + o *Options + + c analytics.Client + l *logrusx.Logger + + mem *MemoryStatistics +} + +// Hash returns a hashed string of the value. +func Hash(value string) string { + sha := sha256.Sum256([]byte(value)) + return hex.EncodeToString(sha[:]) +} + +// Options configures the metrics service. +type Options struct { + // Service represents the service name, for example "ory-hydra". + Service string + + // DeploymentId represents the cluster id, typically a hash of some unique configuration properties. + DeploymentId string + + // DBDialect specifies the database dialect in use (e.g., "postgres", "mysql", "sqlite"). + DBDialect string + + // When this instance was started + StartTime time.Time + + // IsDevelopment should be true if we assume that we're in a development environment. + IsDevelopment bool + + // WriteKey is the segment API key. + WriteKey string + + // WhitelistedPaths represents a list of paths that can be transmitted in clear text to segment. + WhitelistedPaths []string + + // BuildVersion represents the build version. + BuildVersion string + + // BuildHash represents the build git hash. + BuildHash string + + // BuildTime represents the build time. + BuildTime string + + // Hostname is a public URL configured for the service, used to derive hosted name for telemetry. + Hostname string + + // Config overrides the analytics.Config. If nil, sensible defaults will be used. + Config *analytics.Config + + // MemoryInterval sets how often memory statistics should be transmitted. Defaults to every 12 hours. + MemoryInterval time.Duration +} + +type void struct{} + +func (v *void) Logf(format string, args ...interface{}) { +} + +func (v *void) Errorf(format string, args ...interface{}) { +} + +// New returns a new metrics service. If one has been instantiated already, no new instance will be created. +func New( + cmd *cobra.Command, + l *logrusx.Logger, + c *configx.Provider, + o *Options, +) *Service { + lock.Lock() + defer lock.Unlock() + + if instance != nil { + return instance + } + + o.StartTime = time.Now() + + if o.BuildTime == "" { + o.BuildTime = "unknown" + } + + if o.BuildVersion == "" { + o.BuildVersion = "unknown" + } + + if o.BuildHash == "" { + o.BuildHash = "unknown" + } + + if o.Config == nil { + o.Config = &analytics.Config{ + Interval: time.Hour * 6, + } + } + + o.Config.Logger = new(void) + + if o.MemoryInterval < time.Minute { + o.MemoryInterval = time.Hour * 12 + } + + segment, err := analytics.NewWithConfig(o.WriteKey, *o.Config) + if err != nil { + l.WithError(err).Fatalf("Unable to initialise software quality assurance features.") + return nil + } + + optOut, err := cmd.Flags().GetBool("sqa-opt-out") + if err != nil { + cmdx.Must(err, `Unable to get command line flag "sqa-opt-out": %s`, err) + } + + if !optOut { + optOut = c.Bool("sqa.opt_out") + } + + if !optOut { + optOut = c.Bool("sqa_opt_out") + } + + if !optOut { + optOut, _ = strconv.ParseBool(os.Getenv("SQA_OPT_OUT")) + } + + if !optOut { + optOut, _ = strconv.ParseBool(os.Getenv("SQA-OPT-OUT")) + } + + if !optOut { + l.Info("Software quality assurance features are enabled. Learn more at: https://www.ory.com/docs/ecosystem/sqa") + } + + m := &Service{ + optOut: optOut, + instanceId: uuid.Must(uuid.NewV4()).String(), + o: o, + c: segment, + l: l, + mem: new(MemoryStatistics), + } + + instance = m + + go m.Identify() + go m.Track() + + return m +} + +// Identify enables reporting to segment. +func (sw *Service) Identify() { + IdentifySend(sw, true) + + // User has not opt-out then make identify to be sent every 6 hours + if !sw.optOut { + for range time.Tick(time.Hour * 6) { + IdentifySend(sw, false) + } + } +} + +func IdentifySend(sw *Service, startup bool) { + if err := resilience.Retry(sw.l, time.Minute*5, time.Hour*6, func() error { + return sw.c.Enqueue(analytics.Identify{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + + DatabaseDialect: sw.o.DBDialect, + ProductVersion: sw.o.BuildVersion, + ProductBuild: sw.o.BuildHash, + UptimeDeployment: 0, + UptimeInstance: math.Round(time.Since(sw.o.StartTime).Seconds()), + IsDevelopment: sw.o.IsDevelopment, + IsOptOut: sw.optOut, + Startup: startup, + }) + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized environment information") + } +} + +// Track commits memory statistics to segment. +func (sw *Service) Track() { + if sw.optOut { + return + } + + for { + sw.mem.Update() + if err := sw.c.Enqueue(analytics.Track{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + + CPU: runtime.NumCPU(), + OsName: runtime.GOOS, + OsArchitecture: runtime.GOARCH, + Alloc: sw.mem.Alloc, + TotalAlloc: sw.mem.TotalAlloc, + Frees: sw.mem.Frees, + Mallocs: sw.mem.Mallocs, + Lookups: sw.mem.Lookups, + Sys: sw.mem.Sys, + NumGC: sw.mem.NumGC, + HeapAlloc: sw.mem.HeapAlloc, + HeapInuse: sw.mem.HeapInuse, + HeapIdle: sw.mem.HeapIdle, + HeapObjects: sw.mem.HeapObjects, + HeapReleased: sw.mem.HeapReleased, + HeapSys: sw.mem.HeapSys, + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + } + time.Sleep(sw.o.MemoryInterval) + } +} + +// ServeHTTP is a middleware for sending meta information to segment. +func (sw *Service) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + var start time.Time + if !sw.optOut { + start = time.Now() + } + + next(rw, r) + + if sw.optOut { + return + } + + latency := time.Since(start).Milliseconds() + path := sw.anonymizePath(r.URL.Path) + host := urlx.ExtractPublicAddress(sw.o.Hostname, r.Header.Get(XForwardedHostHeader), r.Host) + + // Collecting request info + stat, _ := httpx.GetResponseMeta(rw) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: path, + RequestCode: stat, + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } +} + +func (sw *Service) UnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + var start time.Time + if !sw.optOut { + start = time.Now() + } + + resp, err := handler(ctx, req) + + if sw.optOut { + return resp, err + } + + latency := time.Since(start).Milliseconds() + + hosts := []string{sw.o.Hostname} + if md, ok := metadata.FromIncomingContext(ctx); ok { + for _, h := range knownHeaders { + if v := md.Get(h); len(v) > 0 { + hosts = append(hosts, v[0]) + } + } + } + host := urlx.ExtractPublicAddress(hosts...) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: info.FullMethod, + RequestCode: int(status.Code(err)), + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } + + return resp, err +} + +func (sw *Service) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + if sw.optOut { + return next(ctx, req) + } + + start := time.Now() + resp, err := next(ctx, req) + latency := time.Since(start).Milliseconds() + + hosts := []string{sw.o.Hostname} + for _, h := range append([]string{"Host"}, knownHeaders...) { + if v := req.Header().Get(h); len(v) > 0 { + hosts = append(hosts, v) + } + } + host := urlx.ExtractPublicAddress(hosts...) + + if err := sw.c.Enqueue(analytics.Page{ + InstanceId: sw.instanceId, + DeploymentId: sw.o.DeploymentId, + Project: sw.o.Service, + UrlHost: host, + UrlPath: req.Spec().Procedure, + RequestCode: int(status.Code(err)), + RequestLatency: int(latency), + }); err != nil { + sw.l.WithError(err).Debug("Could not commit anonymized telemetry data") + // do nothing... + } + + return resp, err + } +} + +func (sw *Service) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return next +} +func (sw *Service) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return next +} + +func (sw *Service) StreamInterceptor(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + // this needs a bit of thought, but we don't have streaming RPCs currently anyway + sw.l.Info("The telemetry stream interceptor is not yet implemented!") + return handler(srv, stream) +} + +func (sw *Service) Close() error { + return sw.c.Close() +} + +func (sw *Service) anonymizePath(path string) string { + paths := sw.o.WhitelistedPaths + path = strings.ToLower(path) + + for _, p := range paths { + p = strings.ToLower(p) + if path == p { + return p + } else if len(path) > len(p) && path[:len(p)+1] == p+"/" { + return p + } + } + + return "/" +} + +func (sw *Service) anonymizeQuery(query url.Values, salt string) string { + for _, q := range query { + for i, s := range q { + if s != "" { + s = Hash(s + "|" + salt) + q[i] = s + } + } + } + return query.Encode() +} diff --git a/oryx/migratest/refresh.go b/oryx/migratest/refresh.go new file mode 100644 index 0000000..68583c5 --- /dev/null +++ b/oryx/migratest/refresh.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build refresh +// +build refresh + +package migratest + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func WriteFixtureOnError(t *testing.T, err error, actual interface{}, location string) { + content, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err) + require.NoError(t, os.MkdirAll(filepath.Dir(location), 0777)) + require.NoError(t, os.WriteFile(location, content, 0666)) +} diff --git a/oryx/migratest/run.go b/oryx/migratest/run.go new file mode 100644 index 0000000..0b8d4d8 --- /dev/null +++ b/oryx/migratest/run.go @@ -0,0 +1,41 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migratest + +import ( + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func ContainsExpectedIds(t *testing.T, path string, ids []string) { + files, err := os.ReadDir(path) + require.NoError(t, err) + + for _, f := range files { + if filepath.Ext(f.Name()) == ".json" { + expected := strings.TrimSuffix(filepath.Base(f.Name()), ".json") + assert.Contains(t, ids, expected) + } + } +} + +func CompareWithFixture(t *testing.T, actual interface{}, prefix string, id string) { + location := filepath.Join("fixtures", prefix, id+".json") + //#nosec G304 -- false positive + expected, err := os.ReadFile(location) + WriteFixtureOnError(t, err, actual, location) + + actualJSON, err := json.Marshal(actual) + require.NoError(t, err) + + if !assert.JSONEq(t, string(expected), string(actualJSON)) { + WriteFixtureOnError(t, nil, actual, location) + } +} diff --git a/oryx/migratest/strict.go b/oryx/migratest/strict.go new file mode 100644 index 0000000..3ff9d50 --- /dev/null +++ b/oryx/migratest/strict.go @@ -0,0 +1,17 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !refresh +// +build !refresh + +package migratest + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func WriteFixtureOnError(t *testing.T, err error, actual interface{}, location string) { + require.NoError(t, err) +} diff --git a/oryx/networkx/listener.go b/oryx/networkx/listener.go new file mode 100644 index 0000000..eadfd78 --- /dev/null +++ b/oryx/networkx/listener.go @@ -0,0 +1,31 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "net" + "strings" + + "github.com/ory/x/configx" +) + +func AddressIsUnixSocket(address string) bool { + return strings.HasPrefix(address, "unix:") +} + +func MakeListener(address string, socketPermission *configx.UnixPermission) (net.Listener, error) { + if AddressIsUnixSocket(address) { + addr := strings.TrimPrefix(address, "unix:") + l, err := net.Listen("unix", addr) + if err != nil { + return nil, err + } + err = socketPermission.SetPermission(addr) + if err != nil { + return nil, err + } + return l, nil + } + return net.Listen("tcp", address) +} diff --git a/oryx/networkx/manager.go b/oryx/networkx/manager.go new file mode 100644 index 0000000..707c5f7 --- /dev/null +++ b/oryx/networkx/manager.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "embed" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/sqlcon" +) + +// Migrations of the network manager. Apply by merging with your local migrations using +// fsx.Merge() and then passing all to the migration box. +// +//go:embed migrations/sql/*.sql +var Migrations embed.FS + +func Determine(c *pop.Connection) (*Network, error) { + var p Network + if err := sqlcon.HandleError(c.Q().Order("created_at ASC").First(&p)); err != nil { + if errors.Is(err, sqlcon.ErrNoRows()) { + np := NewNetwork() + if err := c.Create(np); err != nil { + return nil, err + } + return np, nil + } + return nil, err + } + return &p, nil +} diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.cockroach.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql new file mode 100644 index 0000000..beb6b14 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `networks`; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql new file mode 100644 index 0000000..0ba5bfc --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.mysql.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE `networks` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql new file mode 100644 index 0000000..b095b18 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.postgres.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE "networks" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql new file mode 100644 index 0000000..9996f5a --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "networks"; \ No newline at end of file diff --git a/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql new file mode 100644 index 0000000..f808e33 --- /dev/null +++ b/oryx/networkx/migrations/sql/20150100000001000000_networks.sqlite3.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "networks" ( +"id" TEXT PRIMARY KEY, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz b/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz new file mode 100644 index 0000000..e6e32ac --- /dev/null +++ b/oryx/networkx/migrations/templates/20150100000001_networks.down.fizz @@ -0,0 +1 @@ +drop_table("networks") diff --git a/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz b/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz new file mode 100644 index 0000000..52cd069 --- /dev/null +++ b/oryx/networkx/migrations/templates/20150100000001_networks.up.fizz @@ -0,0 +1,3 @@ +create_table("networks") { + t.Column("id", "uuid", {primary: true}) +} diff --git a/oryx/networkx/network.go b/oryx/networkx/network.go new file mode 100644 index 0000000..e9be927 --- /dev/null +++ b/oryx/networkx/network.go @@ -0,0 +1,30 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package networkx + +import ( + "time" + + "github.com/gofrs/uuid" +) + +type Network struct { + ID uuid.UUID `json:"id" db:"id"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` +} + +func (p Network) TableName() string { + return "networks" +} + +func NewNetwork() *Network { + return &Network{ + ID: uuid.Must(uuid.NewV4()), + } +} diff --git a/oryx/openapix/doc.go b/oryx/openapix/doc.go new file mode 100644 index 0000000..654b27f --- /dev/null +++ b/oryx/openapix/doc.go @@ -0,0 +1,6 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package openapi contains definitions commonly used in Ory's APIs +// such as pagination, JSON patches, and more. +package openapix diff --git a/oryx/openapix/jsonpatch.go b/oryx/openapix/jsonpatch.go new file mode 100644 index 0000000..3876906 --- /dev/null +++ b/oryx/openapix/jsonpatch.go @@ -0,0 +1,42 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openapix + +// A JSONPatchDocument request +// +// swagger:model jsonPatchDocument +type JSONPatchDocument []JSONPatch + +// A JSONPatch document as defined by RFC 6902 +// +// swagger:model jsonPatch +type JSONPatch struct { + // The operation to be performed. One of "add", "remove", "replace", "move", "copy", or "test". + // + // required: true + // example: replace + Op string `json:"op"` + + // The path to the target path. Uses JSON pointer notation. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // required: true + // example: /name + Path string `json:"path"` + + // The value to be used within the operations. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // example: foobar + Value interface{} `json:"value"` + + // This field is used together with operation "move" and uses JSON Pointer notation. + // + // Learn more [about JSON Pointers](https://datatracker.ietf.org/doc/html/rfc6901#section-5). + // + // example: /name + From string `json:"from"` +} diff --git a/oryx/openapix/pagination.go b/oryx/openapix/pagination.go new file mode 100644 index 0000000..d906362 --- /dev/null +++ b/oryx/openapix/pagination.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package openapix + +// swagger:model tokenPaginationHeaders +type TokenPaginationHeaders struct { + // The link header contains pagination links. + // + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // in: header + Link string `json:"link"` + + // The total number of clients. + // + // in: header + XTotalCount string `json:"x-total-count"` +} + +// swagger:model tokenPagination +type TokenPaginationParams struct { + // Items per page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} diff --git a/oryx/osx/env.go b/oryx/osx/env.go new file mode 100644 index 0000000..e5462ac --- /dev/null +++ b/oryx/osx/env.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package osx + +import ( + "cmp" + "os" +) + +// GetenvDefault returns an environment variable or the default value if it is empty. +func GetenvDefault(key string, def string) string { + return cmp.Or(os.Getenv(key), def) +} diff --git a/oryx/osx/file.go b/oryx/osx/file.go new file mode 100644 index 0000000..bdb2d30 --- /dev/null +++ b/oryx/osx/file.go @@ -0,0 +1,221 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package osx + +import ( + "encoding/base64" + "io" + "net/url" + "os" + "strings" + + "github.com/hashicorp/go-retryablehttp" + + "github.com/pkg/errors" + + "github.com/ory/x/httpx" +) + +type options struct { + disableFileLoader bool + disableHTTPLoader bool + disableBase64Loader bool + base64enc *base64.Encoding + disableResilientBase64Loader bool + hc *retryablehttp.Client +} + +type Option func(o *options) + +func (o *options) apply(opts []Option) *options { + for _, f := range opts { + f(o) + } + return o +} + +func newOptions() *options { + return &options{ + disableFileLoader: false, + disableHTTPLoader: false, + disableBase64Loader: false, + base64enc: base64.RawURLEncoding, + hc: httpx.NewResilientClient(), + } +} + +// WithDisabledFileLoader disables the file loader. +func WithDisabledFileLoader() Option { + return func(o *options) { + o.disableFileLoader = true + } +} + +// WithEnabledFileLoader enables the file loader. +func WithEnabledFileLoader() Option { + return func(o *options) { + o.disableFileLoader = false + } +} + +// WithDisabledHTTPLoader disables the HTTP loader. +func WithDisabledHTTPLoader() Option { + return func(o *options) { + o.disableHTTPLoader = true + } +} + +// WithEnabledHTTPLoader enables the HTTP loader. +func WithEnabledHTTPLoader() Option { + return func(o *options) { + o.disableHTTPLoader = false + } +} + +// WithDisabledBase64Loader disables the base64 loader. +func WithDisabledBase64Loader() Option { + return func(o *options) { + o.disableBase64Loader = true + } +} + +// WithEnabledBase64Loader disables the base64 loader. +func WithEnabledBase64Loader() Option { + return func(o *options) { + o.disableBase64Loader = false + } +} + +// WithBase64Encoding sets the base64 encoding. +func WithBase64Encoding(enc *base64.Encoding) Option { + return func(o *options) { + o.base64enc = enc + } +} + +// WithoutResilientBase64Encoding sets the base64 encoding. +func WithoutResilientBase64Encoding() Option { + return func(o *options) { + o.disableResilientBase64Loader = true + } +} + +// WithHTTPClient sets the HTTP client. +func WithHTTPClient(hc *retryablehttp.Client) Option { + return func(o *options) { + o.hc = hc + } +} + +// RestrictedReadFile works similar to ReadFileFromAllSources but has all +// sources disabled per default. You need to enable the loaders you wish to use +// explicitly. +func RestrictedReadFile(source string, opts ...Option) (bytes []byte, err error) { + o := newOptions() + o.disableFileLoader = true + o.disableBase64Loader = true + o.disableHTTPLoader = true + return readFile(source, o.apply(opts)) +} + +// ReadFileFromAllSources reads a file from base64, http, https, and file sources. +// +// Using options, you can disable individual loaders. For example, the following will +// return an error: +// +// ReadFileFromAllSources("https://foo.bar/baz.txt", WithDisabledHTTPLoader()) +// +// Possible formats are: +// +// - /path/to/file +// - file:///path/to/file +// - https://host.com/path/to/file +// - http://host.com/path/to/file +// - base64:// +// +// For more options, check: +// +// - WithDisabledFileLoader +// - WithDisabledHTTPLoader +// - WithDisabledBase64Loader +// - WithBase64Encoding +// - WithHTTPClient +func ReadFileFromAllSources(source string, opts ...Option) (bytes []byte, err error) { + return readFile(source, newOptions().apply(opts)) +} + +func readFile(source string, o *options) (bytes []byte, err error) { + parsed, err := url.Parse(source) + if err != nil { + return nil, errors.Wrap(err, "failed to parse URL") + } + + switch parsed.Scheme { + case "": + if o.disableFileLoader { + return nil, errors.New("file loader disabled") + } + + //#nosec G304 -- false positive + bytes, err = os.ReadFile(source) + if err != nil { + return nil, errors.Wrap(err, "unable to read the file") + } + case "file": + if o.disableFileLoader { + return nil, errors.New("file loader disabled") + } + + //#nosec G304 -- false positive + bytes, err = os.ReadFile(parsed.Host + parsed.Path) + if err != nil { + return nil, errors.Wrap(err, "unable to read the file") + } + case "http", "https": + if o.disableHTTPLoader { + return nil, errors.New("http(s) loader disabled") + } + resp, err := o.hc.Get(parsed.String()) + if err != nil { + return nil, errors.Wrap(err, "unable to load remote file") + } + defer resp.Body.Close() + + bytes, err = io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "unable to read the HTTP response body") + } + case "base64": + if o.disableBase64Loader { + return nil, errors.New("base64 loader disabled") + } + + if o.disableResilientBase64Loader { + bytes, err = o.base64enc.DecodeString(strings.TrimPrefix(source, "base64://")) + if err != nil { + return nil, errors.Wrap(err, "unable to base64 decode the location") + } + return bytes, nil + } + + for _, enc := range []*base64.Encoding{ + base64.StdEncoding, + base64.URLEncoding, + base64.RawURLEncoding, + base64.RawStdEncoding, + } { + bytes, err = enc.DecodeString(strings.TrimPrefix(source, "base64://")) + if err == nil { + return bytes, nil + } + } + + return nil, errors.Wrap(err, "unable to base64 decode the location") + default: + return nil, errors.Errorf("unsupported source `%s`", parsed.Scheme) + } + + return bytes, nil + +} diff --git a/oryx/osx/stub/text.txt b/oryx/osx/stub/text.txt new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/oryx/osx/stub/text.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/oryx/otelx/attribute.go b/oryx/otelx/attribute.go new file mode 100644 index 0000000..c84489b --- /dev/null +++ b/oryx/otelx/attribute.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "database/sql" + "fmt" + + "go.opentelemetry.io/otel/attribute" +) + +const nullString = "" + +func StringAttrs(attrs map[string]string) []attribute.KeyValue { + s := make([]attribute.KeyValue, 0, len(attrs)) + for k, v := range attrs { + s = append(s, attribute.String(k, v)) + } + return s +} + +func AutoInt[I int | int32 | int64](k string, v I) attribute.KeyValue { + // Internally, the OpenTelemetry SDK uses int64 for all integer values anyway. + return attribute.Int64(k, int64(v)) +} + +func Nullable[V any, VN *V | sql.Null[V], A func(string, V) attribute.KeyValue](a A, k string, v VN) attribute.KeyValue { + switch v := any(v).(type) { + case *V: + if v == nil { + return attribute.String(k, nullString) + } + return a(k, *v) + case sql.Null[V]: + if !v.Valid { + return attribute.String(k, nullString) + } + return a(k, v.V) + } + // This should never happen, as the type switch above is exhaustive to the generic type VN. + return attribute.String(k, fmt.Sprintf("", v)) +} + +func NullString[V *string | sql.Null[string]](k string, v V) attribute.KeyValue { + return Nullable(attribute.String, k, v) +} + +func NullStringer(k string, v fmt.Stringer) attribute.KeyValue { + if v == nil { + return attribute.String(k, nullString) + } + return attribute.String(k, v.String()) +} diff --git a/oryx/otelx/config.go b/oryx/otelx/config.go new file mode 100644 index 0000000..843f4fe --- /dev/null +++ b/oryx/otelx/config.go @@ -0,0 +1,67 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "bytes" + _ "embed" + "io" +) + +type JaegerConfig struct { + LocalAgentAddress string `json:"local_agent_address"` + Sampling JaegerSampling `json:"sampling"` +} + +type ZipkinConfig struct { + ServerURL string `json:"server_url"` + Sampling ZipkinSampling `json:"sampling"` +} + +type OTLPConfig struct { + ServerURL string `json:"server_url"` + Insecure bool `json:"insecure"` + Sampling OTLPSampling `json:"sampling"` + AuthorizationHeader string `json:"authorization_header"` +} + +type JaegerSampling struct { + ServerURL string `json:"server_url"` + TraceIDRatio float64 `json:"trace_id_ratio"` +} + +type ZipkinSampling struct { + SamplingRatio float64 `json:"sampling_ratio"` +} + +type OTLPSampling struct { + SamplingRatio float64 `json:"sampling_ratio"` +} + +type ProvidersConfig struct { + Jaeger JaegerConfig `json:"jaeger"` + Zipkin ZipkinConfig `json:"zipkin"` + OTLP OTLPConfig `json:"otlp"` +} + +type Config struct { + ServiceName string `json:"service_name"` + DeploymentEnvironment string `json:"deployment_environment"` + Provider string `json:"provider"` + Providers ProvidersConfig `json:"providers"` +} + +//go:embed config.schema.json +var ConfigSchema []byte + +const ConfigSchemaID = "ory://tracing-config" + +// AddConfigSchema adds the tracing schema to the compiler. +// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version. +func AddConfigSchema(c interface { + AddResource(url string, r io.Reader) error +}, +) error { + return c.AddResource(ConfigSchemaID, bytes.NewReader(ConfigSchema)) +} diff --git a/oryx/otelx/config.schema.json b/oryx/otelx/config.schema.json new file mode 100644 index 0000000..1a668f3 --- /dev/null +++ b/oryx/otelx/config.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "ory://tracing-config", + "type": "object", + "additionalProperties": false, + "description": "Configure distributed tracing using OpenTelemetry", + "properties": { + "provider": { + "type": "string", + "description": "Set this to the tracing backend you wish to use. Supports Jaeger, Zipkin, and OTEL.", + "enum": ["jaeger", "otel", "zipkin"], + "examples": ["jaeger"] + }, + "service_name": { + "type": "string", + "description": "Specifies the service name to use on the tracer.", + "examples": ["Ory Hydra", "Ory Kratos", "Ory Keto", "Ory Oathkeeper"] + }, + "deployment_environment": { + "type": "string", + "description": "Specifies the deployment environment to use on the tracer.", + "examples": ["development", "staging", "production"] + }, + "providers": { + "type": "object", + "additionalProperties": false, + "properties": { + "jaeger": { + "type": "object", + "additionalProperties": false, + "description": "Configures the jaeger tracing backend.", + "properties": { + "local_agent_address": { + "type": "string", + "description": "The address of the jaeger-agent where spans should be sent to.", + "anyOf": [ + { + "title": "IPv6 Address and Port", + "pattern": "^\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]:([0-9]*)$" + }, + { + "title": "IPv4 Address and Port", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}:([0-9]*)$" + }, + { + "title": "Hostname and Port", + "pattern": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]):([0-9]*)$" + } + ], + "examples": ["127.0.0.1:6831"] + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["server_url", "trace_id_ratio"] + }, + "additionalProperties": false, + "properties": { + "server_url": { + "type": "string", + "description": "The address of jaeger-agent's HTTP sampling server", + "format": "uri", + "examples": ["http://localhost:5778/sampling"] + }, + "trace_id_ratio": { + "type": "number", + "description": "Trace Id ratio sample", + "examples": [0.5] + } + } + } + } + }, + "zipkin": { + "type": "object", + "additionalProperties": false, + "description": "Configures the zipkin tracing backend.", + "properties": { + "server_url": { + "type": "string", + "description": "The address of the Zipkin server where spans should be sent to.", + "format": "uri", + "examples": ["http://localhost:9411/api/v2/spans"] + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["sampling_ratio"] + }, + "additionalProperties": false, + "properties": { + "sampling_ratio": { + "type": "number", + "description": "Sampling ratio for spans.", + "examples": [0.4] + } + } + } + } + }, + "otlp": { + "type": "object", + "additionalProperties": false, + "description": "Configures the OTLP tracing backend.", + "properties": { + "server_url": { + "type": "string", + "description": "The endpoint of the OTLP exporter (HTTP) where spans should be sent to.", + "anyOf": [ + { + "title": "IPv6 Address and Port", + "pattern": "^\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]:([0-9]*)$" + }, + { + "title": "IPv4 Address and Port", + "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}:([0-9]*)$" + }, + { + "title": "Hostname and Port", + "pattern": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]):([0-9]*)$" + } + ], + "examples": ["localhost:4318"] + }, + "insecure": { + "type": "boolean", + "description": "Will use HTTP if set to true; defaults to HTTPS." + }, + "sampling": { + "type": "object", + "propertyNames": { + "enum": ["sampling_ratio"] + }, + "additionalProperties": false, + "properties": { + "sampling_ratio": { + "type": "number", + "description": "Sampling ratio for spans.", + "examples": [0.4] + } + } + }, + "authorization_header": { + "type": "string", + "examples": ["Bearer 2389s8fs9d8fus9f"] + } + } + } + } + } + } +} diff --git a/oryx/otelx/jaeger.go b/oryx/otelx/jaeger.go new file mode 100644 index 0000000..5da5f77 --- /dev/null +++ b/oryx/otelx/jaeger.go @@ -0,0 +1,88 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "net" + + "go.opentelemetry.io/contrib/propagators/b3" + jaegerPropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/contrib/samplers/jaegerremote" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/jaeger" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +// SetupJaeger configures and returns a Jaeger tracer. +// +// The returned tracer will by default attempt to send spans to a local Jaeger agent. +// Optionally, [otelx.JaegerConfig.LocalAgentAddress] can be set to specify a different target. +// +// By default, unless a parent sampler has taken a sampling decision, every span is sampled. +// [otelx.JaegerSampling.TraceIDRatio] may be used to customize the sampling probability, +// optionally alongside [otelx.JaegerSampling.ServerURL] to consult a remote server +// for the sampling strategy to be used. +func SetupJaeger(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + host, port, err := net.SplitHostPort(c.Providers.Jaeger.LocalAgentAddress) + if err != nil { + return nil, err + } + + exp, err := jaeger.New( + jaeger.WithAgentEndpoint( + jaeger.WithAgentHost(host), jaeger.WithAgentPort(port), + ), + ) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment))), + } + + samplingServerURL := c.Providers.Jaeger.Sampling.ServerURL + traceIdRatio := c.Providers.Jaeger.Sampling.TraceIDRatio + + sampler := sdktrace.TraceIDRatioBased(traceIdRatio) + + if samplingServerURL != "" { + sampler = jaegerremote.New( + "jaegerremote", + jaegerremote.WithSamplingServerURL(samplingServerURL), + jaegerremote.WithInitialSampler(sampler), + ) + } + + // Respect any sampling decision taken by the client. + sampler = sdktrace.ParentBased(sampler) + tpOpts = append(tpOpts, sdktrace.WithSampler(sampler)) + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + // At the moment, software across our cloud stack only support Zipkin (B3) + // and Jaeger propagation formats. Proposals for standardized formats for + // context propagation are in the works (ref: https://www.w3.org/TR/trace-context/ + // and https://www.w3.org/TR/baggage/). + // + // Simply add propagation.TraceContext{} and propagation.Baggage{} + // here to enable those as well. + prop := propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + jaegerPropagator.Jaeger{}, + b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader|b3.B3SingleHeader)), + propagation.Baggage{}, + ) + otel.SetTextMapPropagator(prop) + return tp.Tracer(tracerName), nil +} diff --git a/oryx/otelx/middleware.go b/oryx/otelx/middleware.go new file mode 100644 index 0000000..2b7c6f4 --- /dev/null +++ b/oryx/otelx/middleware.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "cmp" + "net/http" + "strings" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + + "github.com/ory/x/httprouterx" +) + +var withDefaultFilters = otelhttp.WithFilter(func(r *http.Request) bool { + return !(strings.HasPrefix(r.URL.Path, "/health") || + strings.HasPrefix(r.URL.Path, "/admin/health") || + strings.HasPrefix(r.URL.Path, "/metrics") || + strings.HasPrefix(r.URL.Path, "/admin/metrics")) +}) + +func NewMiddleware(next http.Handler, operation string, opts ...otelhttp.Option) http.Handler { + myOpts := []otelhttp.Option{ + withDefaultFilters, + otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string { + return cmp.Or(r.Pattern, operation, r.Method+" "+r.URL.Path) + }), + } + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callback := func(r2 *http.Request) { + r.Pattern = cmp.Or(r.Pattern, r2.Pattern) + } + r2 := httprouterx.WithAfterMatchHook(r, callback) + next.ServeHTTP(w, r2) + r.Pattern = cmp.Or(r2.Pattern, r.Pattern) // best-effort in case callback never is called + }) + return otelhttp.NewHandler(handler, operation, append(myOpts, opts...)...) +} diff --git a/oryx/otelx/otel.go b/oryx/otelx/otel.go new file mode 100644 index 0000000..298e828 --- /dev/null +++ b/oryx/otelx/otel.go @@ -0,0 +1,122 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" + "go.opentelemetry.io/otel/trace/noop" + + "github.com/ory/x/logrusx" + "github.com/ory/x/stringsx" +) + +type ( + Tracer struct { + tracer trace.Tracer + } + Provider interface { + Tracer(ctx context.Context) *Tracer + } +) + +// New creates a new tracer. If name is empty, a default tracer name is used +// instead. See: https://godocs.io/go.opentelemetry.io/otel/sdk/trace#TracerProvider.Tracer +func New(name string, l *logrusx.Logger, c *Config) (*Tracer, error) { + t := &Tracer{} + + if err := t.setup(name, l, c); err != nil { + return nil, err + } + + return t, nil +} + +// NewNoop creates a new no-op tracer. +func NewNoop() *Tracer { + tp := noop.NewTracerProvider() + t := &Tracer{tracer: tp.Tracer("")} + return t +} + +type simpleProvider struct{ t *Tracer } + +func (p simpleProvider) Tracer(_ context.Context) *Tracer { return p.t } + +func NewNoopProvider() Provider { return &simpleProvider{t: NewNoop()} } + +// setup constructs the tracer based on the given configuration. +func (t *Tracer) setup(name string, l *logrusx.Logger, c *Config) error { + switch f := stringsx.SwitchExact(c.Provider); { + case f.AddCase("jaeger"): + tracer, err := SetupJaeger(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("Jaeger tracer configured! Sending spans to %s", c.Providers.Jaeger.LocalAgentAddress) + case f.AddCase("zipkin"): + tracer, err := SetupZipkin(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("Zipkin tracer configured! Sending spans to %s", c.Providers.Zipkin.ServerURL) + case f.AddCase("otel"): + tracer, err := SetupOTLP(t, name, c) + if err != nil { + return err + } + + t.tracer = tracer + l.Infof("OTLP tracer configured! Sending spans to %s", c.Providers.OTLP.ServerURL) + case f.AddCase(""): + l.Infof("No tracer configured - skipping tracing setup") + t.tracer = noop.NewTracerProvider().Tracer(name) + default: + return f.ToUnknownCaseErr() + } + + return nil +} + +// IsLoaded returns true if the tracer has been loaded. +func (t *Tracer) IsLoaded() bool { + if t == nil || t.tracer == nil { + return false + } + return true +} + +// Tracer returns the underlying OpenTelemetry tracer. +func (t *Tracer) Tracer() trace.Tracer { + return t.tracer +} + +// WithOTLP returns a new tracer with the underlying OpenTelemetry Tracer +// replaced. +func (t *Tracer) WithOTLP(other trace.Tracer) *Tracer { + return &Tracer{other} +} + +// Provider returns a TracerProvider which in turn yields this tracer unmodified. +func (t *Tracer) Provider() trace.TracerProvider { + return tracerProvider{t: t.Tracer()} +} + +type tracerProvider struct { + embedded.TracerProvider + t trace.Tracer +} + +var _ trace.TracerProvider = tracerProvider{} + +// Tracer implements trace.TracerProvider. +func (tp tracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { + return tp.t +} diff --git a/oryx/otelx/otlp.go b/oryx/otelx/otlp.go new file mode 100644 index 0000000..f5c3d7d --- /dev/null +++ b/oryx/otelx/otlp.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + + "go.opentelemetry.io/contrib/propagators/b3" + jaegerPropagator "go.opentelemetry.io/contrib/propagators/jaeger" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +func SetupOTLP(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + ctx := context.Background() + + clientOpts := []otlptracehttp.Option{ + otlptracehttp.WithEndpoint(c.Providers.OTLP.ServerURL), + } + + if c.Providers.OTLP.Insecure { + clientOpts = append(clientOpts, otlptracehttp.WithInsecure()) + } + + if c.Providers.OTLP.AuthorizationHeader != "" { + clientOpts = append(clientOpts, + otlptracehttp.WithHeaders(map[string]string{"Authorization": c.Providers.OTLP.AuthorizationHeader}), + ) + } + + exp, err := otlptrace.New( + ctx, otlptracehttp.NewClient(clientOpts...), + ) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment), + )), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased( + c.Providers.OTLP.Sampling.SamplingRatio, + ))), + } + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + jaegerPropagator.Jaeger{}, + b3.New(b3.WithInjectEncoding(b3.B3MultipleHeader|b3.B3SingleHeader)), + propagation.Baggage{}, + )) + + return tp.Tracer(tracerName), nil +} diff --git a/oryx/otelx/semconv/context.go b/oryx/otelx/semconv/context.go new file mode 100644 index 0000000..a67bfd4 --- /dev/null +++ b/oryx/otelx/semconv/context.go @@ -0,0 +1,53 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + "net/http" + + "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/httpx" +) + +type contextKey int + +const contextKeyAttributes contextKey = iota + +func ContextWithAttributes(ctx context.Context, attrs ...attribute.KeyValue) context.Context { + existing, _ := ctx.Value(contextKeyAttributes).([]attribute.KeyValue) + return context.WithValue(ctx, contextKeyAttributes, append(existing, attrs...)) +} + +func AttributesFromContext(ctx context.Context) []attribute.KeyValue { + fromCtx, _ := ctx.Value(contextKeyAttributes).([]attribute.KeyValue) + uniq := make(map[attribute.Key]struct{}) + attrs := make([]attribute.KeyValue, 0) + for i := len(fromCtx) - 1; i >= 0; i-- { + if _, ok := uniq[fromCtx[i].Key]; !ok { + uniq[fromCtx[i].Key] = struct{}{} + attrs = append(attrs, fromCtx[i]) + } + } + reverse(attrs) + return attrs +} + +func Middleware(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + ctx := ContextWithAttributes(r.Context(), + append( + AttrGeoLocation(*httpx.ClientGeoLocation(r)), + AttrClientIP(httpx.ClientIP(r)), + )..., + ) + + next(rw, r.WithContext(ctx)) +} + +func reverse[S ~[]E, E any](s S) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} diff --git a/oryx/otelx/semconv/deprecated.go b/oryx/otelx/semconv/deprecated.go new file mode 100644 index 0000000..4a2615a --- /dev/null +++ b/oryx/otelx/semconv/deprecated.go @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + + otelattr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// NewDeprecatedFeatureUsedEvent creates a new event indicating that a deprecated feature was used. +// It returns the event name and a trace.EventOption that can be used to +// add the event to a span. +// +// span.AddEvent(NewDeprecatedFeatureUsedEvent(ctx, "deprecated-feature-id", otelattr.String("key", "value"))) +func NewDeprecatedFeatureUsedEvent(ctx context.Context, deprecatedCodeFeatureID string, attrs ...otelattr.KeyValue) (string, trace.EventOption) { + return DeprecatedFeatureUsed.String(), + trace.WithAttributes( + append( + append( + attrs, + AttributesFromContext(ctx)..., + ), + AttrDeprecatedFeatureID(deprecatedCodeFeatureID), + )..., + ) +} + +const ( + AttributeKeyDeprecatedCodePathIDAttributeKey AttributeKey = "DeprecatedFeatureID" + DeprecatedFeatureUsed Event = "DeprecatedFeatureUsed" +) + +func AttrDeprecatedFeatureID(id string) otelattr.KeyValue { + return otelattr.String(AttributeKeyDeprecatedCodePathIDAttributeKey.String(), id) +} diff --git a/oryx/otelx/semconv/events.go b/oryx/otelx/semconv/events.go new file mode 100644 index 0000000..7e9759a --- /dev/null +++ b/oryx/otelx/semconv/events.go @@ -0,0 +1,104 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package semconv contains OpenTelemetry semantic convention constants. +package semconv + +import ( + "github.com/gofrs/uuid" + otelattr "go.opentelemetry.io/otel/attribute" + + "github.com/ory/x/httpx" +) + +type Event string + +func (e Event) String() string { + return string(e) +} + +type AttributeKey string + +func (a AttributeKey) String() string { + return string(a) +} + +const ( + AttributeKeyIdentityID AttributeKey = "IdentityID" + AttributeKeyNID AttributeKey = "ProjectID" + AttributeKeyClientIP AttributeKey = "ClientIP" + AttributeKeyGeoLocationCity AttributeKey = "GeoLocationCity" + AttributeKeyGeoLocationRegion AttributeKey = "GeoLocationRegion" + AttributeKeyGeoLocationCountry AttributeKey = "GeoLocationCountry" + AttributeKeyGeoLocationLatitude AttributeKey = "GeoLocationLatitude" + AttributeKeyGeoLocationLongitude AttributeKey = "GeoLocationLongitude" + AttributeKeyWorkspace AttributeKey = "WorkspaceID" + AttributeKeySubscriptionID AttributeKey = "SubscriptionID" + AttributeKeyProjectEnvironment AttributeKey = "ProjectEnvironment" + AttributeKeyWorkspaceAPIKeyID AttributeKey = "WorkspaceAPIKeyID" + AttributeKeyProjectAPIKeyID AttributeKey = "ProjectAPIKeyID" +) + +func AttrIdentityID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyIdentityID.String(), uuidOrString(val)) +} + +func AttrNID(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyNID.String(), val.String()) +} + +func AttrWorkspace(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeyWorkspace.String(), val.String()) +} + +func AttrSubscription(val uuid.UUID) otelattr.KeyValue { + return otelattr.String(AttributeKeySubscriptionID.String(), val.String()) +} + +func AttrProjectEnvironment(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyProjectEnvironment.String(), val) +} + +func AttrClientIP(val string) otelattr.KeyValue { + return otelattr.String(AttributeKeyClientIP.String(), val) +} + +func AttrGeoLocation(val httpx.GeoLocation) []otelattr.KeyValue { + geoLocationAttributes := make([]otelattr.KeyValue, 0, 3) + + if val.City != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationCity.String(), val.City)) + } + if val.Country != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationCountry.String(), val.Country)) + } + if val.Region != "" { + geoLocationAttributes = append(geoLocationAttributes, otelattr.String(AttributeKeyGeoLocationRegion.String(), val.Region)) + } + if val.Latitude != nil { + geoLocationAttributes = append(geoLocationAttributes, otelattr.Float64(AttributeKeyGeoLocationLatitude.String(), *val.Latitude)) + } + if val.Longitude != nil { + geoLocationAttributes = append(geoLocationAttributes, otelattr.Float64(AttributeKeyGeoLocationLongitude.String(), *val.Longitude)) + } + + return geoLocationAttributes +} + +func AttrWorkspaceAPIKeyID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyWorkspaceAPIKeyID.String(), uuidOrString(val)) +} + +func AttrProjectAPIKeyID[V string | uuid.UUID](val V) otelattr.KeyValue { + return otelattr.String(AttributeKeyProjectAPIKeyID.String(), uuidOrString(val)) +} + +func uuidOrString[V string | uuid.UUID](val V) string { + switch val := any(val).(type) { + case string: + return val + case uuid.UUID: + return val.String() + } + panic("unreachable") +} diff --git a/oryx/otelx/semconv/warning.go b/oryx/otelx/semconv/warning.go new file mode 100644 index 0000000..79f9ae4 --- /dev/null +++ b/oryx/otelx/semconv/warning.go @@ -0,0 +1,38 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package semconv + +import ( + "context" + + otelattr "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// NewWarning creates a new warning event with the given ID and attributes. +// It returns the event name and a trace.EventOption that can be used to +// add the event to a span. +// +// span.AddEvent(NewWarning(ctx, "warning-id", otelattr.String("key", "value"))) +func NewWarning(ctx context.Context, id string, attrs ...otelattr.KeyValue) (string, trace.EventOption) { + return Warning.String(), + trace.WithAttributes( + append( + append( + attrs, + AttributesFromContext(ctx)..., + ), + otelattr.String(AttributeWarningID.String(), id), + )..., + ) +} + +const ( + Warning Event = "Warning" + AttributeWarningID AttributeKey = "WarningID" +) + +func AttrWarningID(id string) otelattr.KeyValue { + return otelattr.String(AttributeWarningID.String(), id) +} diff --git a/oryx/otelx/withspan.go b/oryx/otelx/withspan.go new file mode 100644 index 0000000..5fda63d --- /dev/null +++ b/oryx/otelx/withspan.go @@ -0,0 +1,148 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "context" + "errors" + "fmt" + "reflect" + "runtime" + "strings" + + pkgerrors "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +// WithSpan wraps execution of f in a span identified by name. +// +// If f returns an error or panics, the span status will be set to the error +// state. The error (or panic) will be propagated unmodified. +// +// f will be wrapped in a child span by default. To make a new root span +// instead, pass the trace.WithNewRoot() option. +func WithSpan(ctx context.Context, name string, f func(context.Context) error, opts ...trace.SpanStartOption) (err error) { + ctx, span := trace.SpanFromContext(ctx).TracerProvider().Tracer("").Start(ctx, name, opts...) + defer func() { + defer span.End() + if r := recover(); r != nil { + setErrorStatusPanic(span, r) + panic(r) + } else if err != nil { + span.SetStatus(codes.Error, err.Error()) + setErrorTags(span, err) + } + }() + return f(ctx) +} + +// End finishes span, and automatically sets the error state if *err is not nil +// or during panicking. +// +// Usage: +// +// func Divide(ctx context.Context, numerator, denominator int) (ratio int, err error) { +// ctx, span := tracer.Start(ctx, "Divide") +// defer otelx.End(span, &err) +// if denominator == 0 { +// return 0, errors.New("cannot divide by zero") +// } +// return numerator / denominator, nil +// } +// +// During a panic, we don't fully conform to OpenTelemetry's semantic +// conventions because that would require us to emit a span event to attach the +// stacktrace and error type, and we don't want to do that. Instead, we set the +// tags on the span directly. +// https://opentelemetry.io/docs/specs/semconv/exceptions/exceptions-spans/ +// +// For improved compatibility with Datadog, we also set some additional tags as +// documented here: +// https://docs.datadoghq.com/standard-attributes/?product=apm&search=error +func End(span trace.Span, err *error) { + defer span.End() + if r := recover(); r != nil { + setErrorStatusPanic(span, r) + panic(r) + } + if err == nil || *err == nil { + return + } + span.SetStatus(codes.Error, (*err).Error()) + setErrorTags(span, *err) +} + +func setErrorStatusPanic(span trace.Span, recovered any) { + span.SetAttributes( + // OpenTelemetry says to add these attributes to an event, not the span + // itself. We don't want to do that, so we're adding them to the span + // directly. + semconv.ExceptionEscaped(true), + // OpenTelemetry describes "exception.stacktrace" We don't love that, + // though, so we're using "error.stack" instead, like DataDog). + attribute.String("error.stack", stacktrace()), + ) + if t := reflect.TypeOf(recovered); t != nil { + span.SetAttributes(semconv.ExceptionType(t.String())) + } + switch e := recovered.(type) { + case error: + span.SetStatus(codes.Error, "panic: "+e.Error()) + setErrorTags(span, e) + case string, fmt.Stringer: + span.SetStatus(codes.Error, fmt.Sprintf("panic: %v", e)) + default: + span.SetStatus(codes.Error, "panic") + case nil: + // nothing + } +} + +func setErrorTags(span trace.Span, err error) { + span.SetAttributes( + attribute.String("error", err.Error()), + attribute.String("error.message", err.Error()), // DataDog compat + attribute.String("error.type", fmt.Sprintf("%T", errors.Unwrap(err))), // the innermost error type is the most useful here + ) + if e := interface{ StackTrace() pkgerrors.StackTrace }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.stack", fmt.Sprintf("%+v", e.StackTrace()))) + } + if e := interface{ Reason() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.reason", e.Reason())) + } + if e := interface{ Debug() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.debug", e.Debug())) + } + if e := interface{ ID() string }(nil); errors.As(err, &e) { + span.SetAttributes(attribute.String("error.id", e.ID())) + } + if e := interface{ Details() map[string]interface{} }(nil); errors.As(err, &e) { + for k, v := range e.Details() { + span.SetAttributes(attribute.String("error.details."+k, fmt.Sprintf("%v", v))) + } + } +} + +func stacktrace() string { + pc := make([]uintptr, 5) + n := runtime.Callers(4, pc) + if n == 0 { + return "" + } + pc = pc[:n] + frames := runtime.CallersFrames(pc) + + var builder strings.Builder + for { + frame, more := frames.Next() + fmt.Fprintf(&builder, "%s\n\t%s:%d\n", frame.Function, frame.File, frame.Line) + if !more { + break + } + } + return builder.String() +} diff --git a/oryx/otelx/zipkin.go b/oryx/otelx/zipkin.go new file mode 100644 index 0000000..59922c4 --- /dev/null +++ b/oryx/otelx/zipkin.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package otelx + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/zipkin" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.27.0" + "go.opentelemetry.io/otel/trace" +) + +func SetupZipkin(t *Tracer, tracerName string, c *Config) (trace.Tracer, error) { + exp, err := zipkin.New(c.Providers.Zipkin.ServerURL) + if err != nil { + return nil, err + } + + tpOpts := []sdktrace.TracerProviderOption{ + sdktrace.WithBatcher(exp), + sdktrace.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceName(c.ServiceName), + semconv.DeploymentEnvironmentName(c.DeploymentEnvironment), + )), + sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased( + c.Providers.Zipkin.Sampling.SamplingRatio, + ))), + } + + tp := sdktrace.NewTracerProvider(tpOpts...) + otel.SetTracerProvider(tp) + + return tp.Tracer(tracerName), nil +} diff --git a/oryx/package-lock.json b/oryx/package-lock.json new file mode 100644 index 0000000..9ad1dbd --- /dev/null +++ b/oryx/package-lock.json @@ -0,0 +1,657 @@ +{ + "name": "x", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "license-checker": "^25.0.1", + "prettier": "3.8.1" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/dezalgo": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", + "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "dev": true, + "license": "ISC", + "dependencies": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/license-checker": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/license-checker/-/license-checker-25.0.1.tgz", + "integrity": "sha512-mET5AIwl7MR2IAKYYoVBBpV0OnkKQ1xGj2IMMeEFIs42QAkEVjRtFZGWmQ28WeU7MP779iAgOaOy93Mn44mn6g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^2.4.1", + "debug": "^3.1.0", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "read-installed": "~4.0.3", + "semver": "^5.5.0", + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-satisfies": "^4.0.0", + "treeify": "^1.1.0" + }, + "bin": { + "license-checker": "bin/license-checker" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true, + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/read-installed": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/read-installed/-/read-installed-4.0.3.tgz", + "integrity": "sha512-O03wg/IYuV/VtnK2h/KXEt9VIbMUFbk3ERG0Iu4FhLZw0EP0T9znqrYDGn6ncbEsXUFaUjiVAWXHzxwt3lhRPQ==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "slide": "~1.1.3", + "util-extend": "^1.0.1" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.2" + } + }, + "node_modules/read-package-json": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", + "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.1", + "json-parse-even-better-errors": "^2.3.0", + "normalize-package-data": "^2.0.0", + "npm-normalize-package-bin": "^1.0.0" + } + }, + "node_modules/readdir-scoped-modules": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz", + "integrity": "sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "license": "ISC", + "dependencies": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "*" + } + }, + "node_modules/spdx-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", + "integrity": "sha512-C1mDZOX0hnu0ep9dfmuoi03+eOdDoz2yvK79RxbcrVEG1NO1Ph35yW102DHWKN4pk80nwCgeMmSY5L25VE4D9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-find-index": "^1.0.2", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.22", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz", + "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdx-ranges": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/spdx-ranges/-/spdx-ranges-2.1.1.tgz", + "integrity": "sha512-mcdpQFV7UDAgLpXEE/jOMqvK4LBoO0uTQg0uvXUewmEFhpiZx5yJSZITHB8w1ZahKdhfZqP5GPEOKLyEq5p8XA==", + "dev": true, + "license": "(MIT AND CC-BY-3.0)" + }, + "node_modules/spdx-satisfies": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/spdx-satisfies/-/spdx-satisfies-4.0.1.tgz", + "integrity": "sha512-WVzZ/cXAzoNmjCWiEluEA3BjHp5tiUmmhn9MK+X0tBbR9sOqtC6UQwmgCNrAIZvNlMuBUYAaHYfb2oqlF9SwKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-compare": "^1.0.0", + "spdx-expression-parse": "^3.0.0", + "spdx-ranges": "^2.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/treeify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/treeify/-/treeify-1.1.0.tgz", + "integrity": "sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/util-extend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/util-extend/-/util-extend-1.0.3.tgz", + "integrity": "sha512-mLs5zAK+ctllYBj+iAQvlDCwoxU/WDOUaJkcFudeiAX6OajC6BKXJUa9a+tbtkC11dz2Ufb7h0lyvIOVn4LADA==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/oryx/package.go b/oryx/package.go new file mode 100644 index 0000000..5b760d6 --- /dev/null +++ b/oryx/package.go @@ -0,0 +1,4 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package x diff --git a/oryx/package.json b/oryx/package.json new file mode 100644 index 0000000..6a0de08 --- /dev/null +++ b/oryx/package.json @@ -0,0 +1,12 @@ +{ + "private": true, + "prettier": { + "proseWrap": "always", + "semi": false, + "trailingComma": "all" + }, + "devDependencies": { + "license-checker": "^25.0.1", + "prettier": "3.8.1" + } +} diff --git a/oryx/pagination/README.md b/oryx/pagination/README.md new file mode 100644 index 0000000..69ba0e0 --- /dev/null +++ b/oryx/pagination/README.md @@ -0,0 +1,29 @@ +# pagination + +A simple helper for dealing with pagination. + +``` +go get github.com/ory/pagination +``` + +## Example + +```go +package main + +import ( + "github.com/ory/pagination" + "net/http" + "net/url" + "fmt" +) + +func main() { + u, _ := url.Parse("http://localhost/foo?offset=0&limit=10") + limit, offset := pagination.Parse(&http.Request{URL: u}, 5, 5, 10) + + items := []string{"a", "b", "c", "d"} + start, end := pagination.Index(limit, offset, len(items)) + fmt.Printf("Got items: %v", items[start:end]) +} +``` diff --git a/oryx/pagination/header.go b/oryx/pagination/header.go new file mode 100644 index 0000000..17533e5 --- /dev/null +++ b/oryx/pagination/header.go @@ -0,0 +1,96 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +import ( + "fmt" + "math" + "net/http" + "net/url" + "strconv" + "strings" +) + +func header(u *url.URL, rel string, limit, offset int64) string { + q := u.Query() + q.Set("limit", fmt.Sprintf("%d", limit)) + q.Set("offset", fmt.Sprintf("%d", offset)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +type formatter func(location *url.URL, rel string, itemsPerPage int64, offset int64) string + +// HeaderWithFormatter adds an HTTP header for pagination which uses a custom formatter for generating the URL links. +func HeaderWithFormatter(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int, f formatter) { + if itemsPerPage <= 0 { + itemsPerPage = 1 + } + + itemsPerPage64 := int64(itemsPerPage) + offset := int64(page) * itemsPerPage64 + + // lastOffset will either equal the offset required to contain the remainder, + // or the limit. + var lastOffset int64 + if total%itemsPerPage64 == 0 { + lastOffset = total - itemsPerPage64 + } else { + lastOffset = (total / itemsPerPage64) * itemsPerPage64 + } + + w.Header().Set("X-Total-Count", strconv.FormatInt(total, 10)) + + // Check for last page + if offset >= lastOffset { + if total == 0 { + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64), + f(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64), + }, ",")) + return + } + + if total <= itemsPerPage64 { + w.Header().Set("link", f(u, "first", total, 0)) + return + } + + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "prev", itemsPerPage64, lastOffset-itemsPerPage64), + }, ",")) + return + } + + if offset < itemsPerPage64 { + w.Header().Set("Link", strings.Join([]string{ + f(u, "next", itemsPerPage64, itemsPerPage64), + f(u, "last", itemsPerPage64, lastOffset), + }, ",")) + return + } + + w.Header().Set("Link", strings.Join([]string{ + f(u, "first", itemsPerPage64, 0), + f(u, "next", itemsPerPage64, ((offset/itemsPerPage64)+1)*itemsPerPage64), + f(u, "prev", itemsPerPage64, ((offset/itemsPerPage64)-1)*itemsPerPage64), + f(u, "last", itemsPerPage64, lastOffset), + }, ",")) +} + +// Header adds an http header for pagination using a responsewriter where backwards compatibility is required. +// The header will contain links any combination of the first, last, next, or previous (prev) pages in a paginated list (given a limit and an offset, and optionally a total). +// If total is not set, then no "last" page will be calculated. +// If no limit is provided, then it will default to 1. +func Header(w http.ResponseWriter, u *url.URL, total int, limit, offset int) { + var page int + if limit == 0 { + limit = 1 + } + + page = int(math.Floor(float64(offset) / float64(limit))) + HeaderWithFormatter(w, u, int64(total), page, limit, header) +} diff --git a/oryx/pagination/items.go b/oryx/pagination/items.go new file mode 100644 index 0000000..6094f2a --- /dev/null +++ b/oryx/pagination/items.go @@ -0,0 +1,12 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +// MaxItemsPerPage is used to prevent DoS attacks against large lists by limiting the items per page to 500. +func MaxItemsPerPage(max, is int) int { + if is > max { + return max + } + return is +} diff --git a/oryx/pagination/keysetclient/parse_header.go b/oryx/pagination/keysetclient/parse_header.go new file mode 100644 index 0000000..4ab09fd --- /dev/null +++ b/oryx/pagination/keysetclient/parse_header.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package keysetclient parses keyset pagination metadata from HTTP responses. +// It is the client-side counterpart to keysetpagination and deliberately +// carries no SQL dependencies, so API clients such as the Ory CLI do not link +// the database layer. +package keysetclient + +import ( + "net/http" + "net/url" + + "github.com/peterhellberg/link" +) + +// Result represents a parsed result of the link HTTP header. +type Result struct { + // NextToken is the next page token. If it's empty, there is no next page. + NextToken string + + // FirstToken is the first page token. + FirstToken string +} + +// ParseHeader parses the response header's Link. +func ParseHeader(r *http.Response) *Result { + links := link.ParseResponse(r) + return &Result{ + NextToken: findRel(links, "next"), + FirstToken: findRel(links, "first"), + } +} + +func findRel(links link.Group, rel string) string { + for idx, l := range links { + if idx == rel { + parsed, err := url.Parse(l.URI) + if err != nil { + continue + } + + return parsed.Query().Get("page_token") + } + } + + return "" +} diff --git a/oryx/pagination/keysetpagination/header.go b/oryx/pagination/keysetpagination/header.go new file mode 100644 index 0000000..ec16640 --- /dev/null +++ b/oryx/pagination/keysetpagination/header.go @@ -0,0 +1,112 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="next" + // + Link string `json:"link"` +} + +func header(u *url.URL, rel, token string, size int) string { + q := u.Query() + q.Set("page_token", token) + q.Set("page_size", strconv.Itoa(size)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +// Header adds the Link header for the page encoded by the paginator. +// It contains links to the first and next page, if one exists. +func Header(w http.ResponseWriter, u *url.URL, p *Paginator) { + size := p.Size() + link := []string{header(u, "first", p.defaultToken.Encode(), size)} + if !p.isLast { + link = append(link, header(u, "next", p.Token().Encode(), size)) + } + w.Header().Set("Link", strings.Join(link, ",")) +} + +// Parse returns the pagination options from the URL query. +func Parse(q url.Values, p PageTokenConstructor) ([]Option, error) { + var opts []Option + if pt := cmp.Or(q["page_token"]...); pt != "" { + pageToken, err := url.QueryUnescape(pt) + if err != nil { + return nil, errors.WithStack(err) + } + parsed, err := p(pageToken) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithToken(parsed)) + } + if ps := cmp.Or(q["page_size"]...); ps != "" { + size, err := strconv.Atoi(ps) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithSize(size)) + } + return opts, nil +} diff --git a/oryx/pagination/keysetpagination/page_token.go b/oryx/pagination/keysetpagination/page_token.go new file mode 100644 index 0000000..c881678 --- /dev/null +++ b/oryx/pagination/keysetpagination/page_token.go @@ -0,0 +1,74 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "encoding/base64" + "fmt" + "strings" +) + +type PageToken = interface { + Parse(string) map[string]string + Encode() string +} + +var _ PageToken = new(StringPageToken) +var _ PageToken = new(MapPageToken) + +type StringPageToken string + +func (s StringPageToken) Parse(idField string) map[string]string { + return map[string]string{idField: string(s)} +} + +func (s StringPageToken) Encode() string { + return string(s) +} + +func NewStringPageToken(s string) (PageToken, error) { + return StringPageToken(s), nil +} + +type MapPageToken map[string]string + +func (m MapPageToken) Parse(_ string) map[string]string { + return map[string]string(m) +} + +const pageTokenColumnDelim = "/" + +func (m MapPageToken) Encode() string { + elems := make([]string, 0, len(m)) + for k, v := range m { + elems = append(elems, fmt.Sprintf("%s=%s", k, v)) + } + + // For now: use Base64 instead of URL escaping, as the Timestamp format we need to use can contain a `+` sign, + // which represents a space in URLs, so it's not properly encoded by the Go library. + return base64.RawStdEncoding.EncodeToString([]byte(strings.Join(elems, pageTokenColumnDelim))) +} + +func NewMapPageToken(s string) (PageToken, error) { + b, err := base64.RawStdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + tokens := strings.Split(string(b), pageTokenColumnDelim) + + r := map[string]string{} + + for _, p := range tokens { + if columnName, value, found := strings.Cut(p, "="); found { + r[columnName] = value + } + } + + return MapPageToken(r), nil +} + +var _ PageTokenConstructor = NewMapPageToken +var _ PageTokenConstructor = NewStringPageToken + +type PageTokenConstructor = func(string) (PageToken, error) diff --git a/oryx/pagination/keysetpagination/paginator.go b/oryx/pagination/keysetpagination/paginator.go new file mode 100644 index 0000000..8f47562 --- /dev/null +++ b/oryx/pagination/keysetpagination/paginator.go @@ -0,0 +1,258 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "errors" + "fmt" + + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/columns" +) + +type ( + Item = interface{ PageToken() PageToken } + + Order string + + columnOrdering struct { + name string + order Order + } + Paginator struct { + token, defaultToken PageToken + size, defaultSize, maxSize int + isLast bool + additionalColumn columnOrdering + } + Option func(*Paginator) *Paginator +) + +var ErrUnknownOrder = errors.New("unknown order") + +const ( + OrderDescending Order = "DESC" + OrderAscending Order = "ASC" + + DefaultSize = 100 + DefaultMaxSize = 500 +) + +func (o Order) extract() (string, string, error) { + switch o { + case OrderAscending: + return ">", string(o), nil + case OrderDescending: + return "<", string(o), nil + default: + return "", "", ErrUnknownOrder + } +} + +func (p *Paginator) Token() PageToken { + if p.token == nil { + return p.defaultToken + } + return p.token +} + +func (p *Paginator) Size() int { + size := p.size + if size <= 0 { + size = p.defaultSize + if size == 0 { + size = 100 + } + } + if size > p.maxSize { + size = p.maxSize + } + return size +} + +func (p *Paginator) IsLast() bool { + return p.isLast +} + +func (p *Paginator) ToOptions() []Option { + opts := make([]Option, 0, 7) + if p.token != nil { + opts = append(opts, WithToken(p.token)) + } + if p.defaultToken != nil { + opts = append(opts, WithDefaultToken(p.defaultToken)) + } + if p.size > 0 { + opts = append(opts, WithSize(p.size)) + } + if p.defaultSize != DefaultSize { + opts = append(opts, WithDefaultSize(p.defaultSize)) + } + if p.maxSize != DefaultMaxSize { + opts = append(opts, WithMaxSize(p.maxSize)) + } + if p.additionalColumn.name != "" { + opts = append(opts, WithColumn(p.additionalColumn.name, p.additionalColumn.order)) + } + if p.isLast { + opts = append(opts, withIsLast(p.isLast)) + } + return opts +} + +func (p *Paginator) multipleOrderFieldsQuery(q *pop.Query, idField string, cols map[string]*columns.Column, quoteAndContextualize func(string) string) { + tokenParts := p.Token().Parse(idField) + idValue := tokenParts[idField] + + column, ok := cols[p.additionalColumn.name] + if !ok { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + quoteName := quoteAndContextualize(column.Name) + + value, ok := tokenParts[column.Name] + + if !ok { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + sign, keyword, err := p.additionalColumn.order.extract() + if err != nil { + q.Where(fmt.Sprintf(`%s > ?`, quoteAndContextualize(idField)), idValue) + return + } + + q. + Where(fmt.Sprintf("(%s %s ? OR (%s = ? AND %s > ?))", quoteName, sign, quoteName, quoteAndContextualize(idField)), value, value, idValue). + Order(fmt.Sprintf("%s %s", quoteName, keyword)) + +} + +// Paginate returns a function that paginates a pop.Query. +// Usage: +// +// q := c.Where("foo = ?", foo).Scope(keysetpagination.Paginate[MyItemType](paginator)) +// +// This function works regardless of whether your type implements the Item +// interface with pointer or value receivers. To understand the type parameters, +// see this document: +// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example +func Paginate[I any, PI interface { + Item + *I +}](p *Paginator) pop.ScopeFunc { + model := pop.Model{Value: new(I)} + id := model.IDField() + tableName := model.Alias() + return func(q *pop.Query) *pop.Query { + quote := q.Connection.Dialect.Quote + eid := quote(tableName) + "." + quote(id) + + quoteAndContextualize := func(name string) string { + return quote(tableName) + "." + quote(name) + } + p.multipleOrderFieldsQuery(q, id, model.Columns().Cols, quoteAndContextualize) + + return q. + Limit(p.Size() + 1). + // we always need to order by the id field last + Order(fmt.Sprintf(`%s ASC`, eid)) + } +} + +// Result removes the last item (if applicable) and returns the paginator for the next page. +// +// This function works regardless of whether your type implements the Item +// interface with pointer or value receivers. To understand the type parameters, +// see this document: +// https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#pointer-method-example +func Result[I any, PI interface { + Item + *I +}](items []I, p *Paginator) ([]I, *Paginator) { + if len(items) > p.Size() { + items = items[:p.Size()] + return items, &Paginator{ + token: PI(&items[len(items)-1]).PageToken(), + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } + } + return items, &Paginator{ + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + isLast: true, + } +} + +func WithDefaultToken(t PageToken) Option { + return func(opts *Paginator) *Paginator { + opts.defaultToken = t + return opts + } +} + +func WithDefaultSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.defaultSize = size + return opts + } +} + +func WithMaxSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.maxSize = size + return opts + } +} + +func WithToken(t PageToken) Option { + return func(opts *Paginator) *Paginator { + opts.token = t + return opts + } +} + +func WithSize(size int) Option { + return func(opts *Paginator) *Paginator { + opts.size = size + return opts + } +} + +func WithColumn(name string, order Order) Option { + return func(opts *Paginator) *Paginator { + opts.additionalColumn = columnOrdering{ + name: name, + order: order, + } + return opts + } +} + +func withIsLast(isLast bool) Option { + return func(opts *Paginator) *Paginator { + opts.isLast = isLast + return opts + } +} + +func GetPaginator(modifiers ...Option) *Paginator { + opts := &Paginator{ + // these can still be overridden by the modifiers, but they should never be unset + maxSize: DefaultMaxSize, + defaultSize: DefaultSize, + } + for _, f := range modifiers { + opts = f(opts) + } + return opts +} diff --git a/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json b/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json new file mode 100644 index 0000000..8fd784f --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/.snapshots/TestPageToken-Marshal_snapshot.json @@ -0,0 +1,67 @@ +{ + "e": "2023-01-01T01:00:00Z", + "c": [ + { + "n": "id", + "v": "token" + }, + { + "n": "string", + "o": 1, + "v": "str" + }, + { + "n": "string-zero", + "o": 1, + "v": "" + }, + { + "n": "time", + "vt": "2023-01-01T00:00:00Z" + }, + { + "n": "uuid", + "vu": "b652bbbd-1a84-4fb7-9437-6b78ba2686db" + }, + { + "n": "nulluuid", + "vu": "b652bbbd-1a84-4fb7-9437-6b78ba2686da" + }, + { + "n": "int64", + "vi": 64 + }, + { + "n": "int64-zero", + "vi": 0 + }, + { + "n": "empty-nullint64", + "vi": 0 + }, + { + "n": "nullint64", + "vi": 64 + }, + { + "n": "nullstring", + "v": "str" + }, + { + "n": "empty-nullstring", + "v": "" + }, + { + "n": "null-nulluuid", + "vn": true + }, + { + "n": "null-nullstring", + "vn": true + }, + { + "n": "null-nullint64", + "vn": true + } + ] +} diff --git a/oryx/pagination/keysetpagination_v2/page_token.go b/oryx/pagination/keysetpagination_v2/page_token.go new file mode 100644 index 0000000..f468002 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/page_token.go @@ -0,0 +1,245 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "crypto/rand" + "database/sql" + "encoding/base64" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/x/aead" +) + +// pageTokenContext is passed as additional authenticated data to the AEAD. +// It binds the ciphertext to its purpose: even if the encryption key is +// reused in another context (for example when it is derived from a shared +// system secret), ciphertexts from that context are rejected as page tokens +// and vice versa. +const pageTokenContext = "ory/keysetpagination_v2/page_token" + +// fallbackEncryptionKey seals page tokens when no pagination secrets are +// configured. It is well known, so those tokens are only obfuscated, not +// authenticated. Configure pagination secrets to authenticate tokens. +var fallbackEncryptionKey [32]byte + +type ( + PageToken struct { + testNow func() time.Time + cols []Column + } + jsonPageToken = struct { + ExpiresAt time.Time `json:"e"` + Cols []jsonColumn `json:"c"` + } + + jsonColumn = struct { + Name string `json:"n"` + Order Order `json:"o,omitempty"` + Nullable bool `json:"nl,omitempty"` + HasConstraint bool `json:"hc,omitempty"` + + ValueAny any `json:"v,omitempty"` + ValueTime *time.Time `json:"vt,omitempty"` + ValueUUID *uuid.UUID `json:"vu,omitempty"` + ValueInt64 *int64 `json:"vi,omitempty"` + ValueNull bool `json:"vn,omitempty"` + } + Column struct { + Name string + Order Order + Value any + Nullable bool + // HasConstraint marks if the column is already constrained by WHERE condition. + HasConstraint bool + } +) + +func (t PageToken) Columns() []Column { return t.cols } + +// Encrypt encrypts the page token using the first key in the provided keyset. +// It uses a fallback key if no keys are provided. +func (t PageToken) Encrypt(keys [][32]byte) string { + key := fallbackEncryptionKey + if len(keys) > 0 { + key = keys[0] + } + enc, err := t.encrypt(key) + if err != nil { + // This should basically never happen, only if reading from the random source or marshaling the token fails. + // In both cases, we have a bigger problem than just not being able to generate the page token. + // Therefore, if we do get an error, there is no point in returning it to the client, + // as we already have a working result set. With this string, the next page will return an error, + // but that is better than breaking the current page. + return "internal error: failed to generate page token" + } + return enc +} + +func (t PageToken) MarshalJSON() ([]byte, error) { + now := time.Now + if t.testNow != nil { + now = t.testNow + } + toEncode := jsonPageToken{ + ExpiresAt: now().Add(time.Hour).UTC(), + Cols: make([]jsonColumn, len(t.cols)), + } + for i, col := range t.cols { + toEncode.Cols[i] = jsonColumn{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + } + switch v := col.Value.(type) { + case time.Time: + toEncode.Cols[i].ValueTime = new(v) + case uuid.UUID: + toEncode.Cols[i].ValueUUID = new(v) + case uuid.NullUUID: + if v.Valid { + toEncode.Cols[i].ValueUUID = new(v.UUID) + } else { + toEncode.Cols[i].ValueNull = true + } + case sql.NullString: + if v.Valid { + toEncode.Cols[i].ValueAny = new(v.String) + } else { + toEncode.Cols[i].ValueNull = true + } + case int64: + toEncode.Cols[i].ValueInt64 = new(v) + case sql.NullInt64: + if v.Valid { + toEncode.Cols[i].ValueInt64 = new(v.Int64) + } else { + toEncode.Cols[i].ValueNull = true + } + default: + toEncode.Cols[i].ValueAny = v + } + } + return json.Marshal(toEncode) +} + +func ErrPageTokenExpired() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("page token expired, do not persist page tokens") +} + +func (t *PageToken) UnmarshalJSON(data []byte) error { + rawToken := jsonPageToken{} + if err := json.Unmarshal(data, &rawToken); err != nil { + return err + } + t.cols = make([]Column, len(rawToken.Cols)) + for i, col := range rawToken.Cols { + t.cols[i] = Column{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + } + switch { + case col.ValueNull: + t.cols[i].Value = nil + case col.ValueAny != nil: + t.cols[i].Value = col.ValueAny + case col.ValueUUID != nil: + t.cols[i].Value = *col.ValueUUID + case col.ValueTime != nil: + t.cols[i].Value = *col.ValueTime + case col.ValueInt64 != nil: + t.cols[i].Value = *col.ValueInt64 + } + } + + now := time.Now + if t.testNow != nil { + now = t.testNow + } + if rawToken.ExpiresAt.Before(now().UTC()) { + return errors.WithStack(ErrPageTokenExpired()) + } + return nil +} + +func NewPageToken(cols ...Column) PageToken { return PageToken{cols: cols} } + +func (t *PageToken) encrypt(key [32]byte) (string, error) { + raw, err := json.Marshal(t) + if err != nil { + return "", errors.Wrap(err, "cannot marshal page token") + } + + a, err := aead.New(key) + if err != nil { + return "", errors.Wrap(err, "cannot create AEAD") + } + + // The nonce is prepended to the ciphertext. AEADs that manage the nonce + // internally report a nonce size of zero, so this also covers them. + nonce := make([]byte, a.NonceSize()) + if _, err := rand.Read(nonce); err != nil { + return "", errors.Wrap(err, "cannot generate nonce") + } + + return base64.URLEncoding.EncodeToString(a.Seal(nonce, nonce, raw, []byte(pageTokenContext))), nil +} + +func (t *PageToken) decrypt(key [32]byte, s string) error { + if s == "" { + return errors.WithStack(ErrInvalidPaginationToken()) + } + + raw, err := base64.URLEncoding.DecodeString(s) + if err != nil { + return errors.WithStack(ErrInvalidPaginationToken()) + } + + dec, err := openAEAD(key, raw) + if err != nil { + // Tokens issued before the switch to a context-bound AEAD are sealed + // with NaCl secretbox. Remove this fallback once all tokens issued by + // the previous version have expired. + var ok bool + dec, ok = aead.OpenLegacySecretbox(key, raw) + if !ok { + return errors.WithStack(ErrInvalidPaginationToken()) + } + } + + if err := json.Unmarshal(dec, t); err != nil { + if errors.As(err, new(*herodot.DefaultError)) { + return err + } + return errors.WithStack(herodot.ErrInternalServerError().WithReason("unable to unmarshal page token").WithDebug(err.Error())) + } + + return nil +} + +func openAEAD(key [32]byte, raw []byte) ([]byte, error) { + a, err := aead.New(key) + if err != nil { + return nil, errors.Wrap(err, "cannot create AEAD") + } + if len(raw) < a.NonceSize() { + return nil, errors.New("ciphertext too short") + } + nonce, ciphertext := raw[:a.NonceSize()], raw[a.NonceSize():] + bs, err := a.Open(nil, nonce, ciphertext, []byte(pageTokenContext)) + if err != nil { + return nil, errors.Wrap(err, "cannot open AEAD") + } + + return bs, nil +} diff --git a/oryx/pagination/keysetpagination_v2/paginator.go b/oryx/pagination/keysetpagination_v2/paginator.go new file mode 100644 index 0000000..0722f1d --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/paginator.go @@ -0,0 +1,192 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "reflect" + + "github.com/jmoiron/sqlx/reflectx" + + "github.com/ory/herodot" +) + +func ErrInvalidPaginationToken() *herodot.DefaultError { + return herodot.ErrBadRequest().WithError("invalid pagination token") +} + +type ( + Paginator struct { + token, defaultToken PageToken + size, defaultSize, maxSize int + isLast bool + } + Option func(*Paginator) +) + +const ( + DefaultSize = 100 + DefaultMaxSize = 500 +) + +var dbStructTagMapper = reflectx.NewMapper("db") + +func (p *Paginator) DefaultToken() PageToken { return p.defaultToken } +func (p *Paginator) IsLast() bool { return p.isLast } + +func (p *Paginator) PageToken() PageToken { + if p.token.cols != nil { + return p.token + } + return p.defaultToken +} + +func (p *Paginator) Size() int { + defaultSize := cmp.Or(p.defaultSize, DefaultSize) + maxSize := cmp.Or(p.maxSize, DefaultMaxSize) + + size := p.size + if size <= 0 { + size = defaultSize + } + if size > maxSize { + size = maxSize + } + + return size +} + +func (p *Paginator) ToOptions() []Option { + opts := make([]Option, 0, 6) + if p.token.cols != nil { + opts = append(opts, WithToken(p.token)) + } + if p.defaultToken.cols != nil { + opts = append(opts, WithDefaultToken(p.defaultToken)) + } + if p.size > 0 { + opts = append(opts, WithSize(p.size)) + } + if p.defaultSize != DefaultSize { + opts = append(opts, WithDefaultSize(p.defaultSize)) + } + if p.maxSize != DefaultMaxSize { + opts = append(opts, WithMaxSize(p.maxSize)) + } + if p.isLast { + opts = append(opts, withIsLast(p.isLast)) + } + return opts +} + +// Result removes the last item (if applicable) and returns the paginator for the next page. +func Result[I any](items []I, p *Paginator) ([]I, *Paginator) { + return ResultFunc(items, p, func(last I, colName string) any { + lastItemVal := reflect.ValueOf(last) + return dbStructTagMapper.FieldByName(lastItemVal, colName).Interface() + }) +} + +// ResultFunc removes the last item (if applicable) and returns the paginator for the next page. +// The extractor function is used to extract the column values from the last item. +func ResultFunc[I any](items []I, p *Paginator, extractor func(last I, colName string) any) ([]I, *Paginator) { + if len(items) <= p.Size() { + return items, &Paginator{ + isLast: true, + + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } + } + + items = items[:p.Size()] + lastItem := items[len(items)-1] + + currentCols := p.PageToken().Columns() + newCols := make([]Column, len(currentCols)) + for i, col := range currentCols { + newCols[i] = Column{ + Name: col.Name, + Order: col.Order, + Nullable: col.Nullable, + HasConstraint: col.HasConstraint, + Value: extractor(lastItem, col.Name), + } + } + + return items, &Paginator{ + token: NewPageToken(newCols...), + defaultToken: p.defaultToken, + size: p.size, + defaultSize: p.defaultSize, + maxSize: p.maxSize, + } +} + +func WithSize(size int) Option { + return func(p *Paginator) { p.size = size } +} + +func WithDefaultSize(size int) Option { + return func(p *Paginator) { p.defaultSize = size } +} + +func WithMaxSize(size int) Option { + return func(p *Paginator) { p.maxSize = size } +} + +func WithToken(t PageToken) Option { + return func(p *Paginator) { p.token = t } +} + +func WithDefaultToken(t PageToken) Option { + return func(p *Paginator) { p.defaultToken = t } +} + +func withIsLast(isLast bool) Option { + return func(p *Paginator) { p.isLast = isLast } +} + +func NewPaginator(modifiers ...Option) (*Paginator, error) { + p := &Paginator{ + // these can still be overridden by the modifiers, but they should never be unset + maxSize: DefaultMaxSize, + defaultSize: DefaultSize, + } + for _, f := range modifiers { + f(p) + } + if err := p.validatePageToken(); err != nil { + return nil, err + } + return p, nil +} + +// validatePageToken ensures the page token columns strictly match DefaultToken's schema in order. +func (p *Paginator) validatePageToken() error { + tokenCols := p.PageToken().Columns() + if len(tokenCols) == 0 { + return nil + } + + if columnsMatch(tokenCols, p.defaultToken.Columns()) { + return nil + } + return ErrInvalidPaginationToken() +} + +func columnsMatch(tokenCols, templateCols []Column) bool { + if len(templateCols) == 0 || len(tokenCols) != len(templateCols) { + return false + } + for i, col := range tokenCols { + def := templateCols[i] + if def.Name != col.Name || def.Order != col.Order || def.Nullable != col.Nullable { + return false + } + } + return true +} diff --git a/oryx/pagination/keysetpagination_v2/parse_header.go b/oryx/pagination/keysetpagination_v2/parse_header.go new file mode 100644 index 0000000..a9847f1 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/parse_header.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "net/http" + "net/url" + + "github.com/peterhellberg/link" +) + +// ParseHeader parses the response header's Link and returns the first and next page tokens. +func ParseHeader(r *http.Response) (first, next string, isLast bool) { + links := link.ParseResponse(r) + first, _ = findRel(links, "first") + next, hasNext := findRel(links, "next") + return first, next, !hasNext +} + +func findRel(links link.Group, rel string) (string, bool) { + for idx, l := range links { + if idx == rel { + parsed, err := url.Parse(l.URI) + if err != nil { + continue + } + q := parsed.Query() + + return q.Get("page_token"), q.Has("page_token") + } + } + + return "", false +} diff --git a/oryx/pagination/keysetpagination_v2/query_builder.go b/oryx/pagination/keysetpagination_v2/query_builder.go new file mode 100644 index 0000000..801005e --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/query_builder.go @@ -0,0 +1,163 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "database/sql/driver" + "fmt" + "strings" + + "github.com/ory/pop/v6" +) + +type Order int + +const ( + OrderAscending Order = iota + OrderDescending +) + +func (o Order) extract() (string, string) { + switch o { + case OrderAscending: + return ">", "ASC" + case OrderDescending: + return "<", "DESC" + default: + panic(fmt.Sprintf("keyset pagination: unknown order %d", o)) + } +} + +// Paginate returns a function that paginates a pop.Query. +// Usage: +// +// q := c.Where("foo = ?", foo).Scope(keysetpagination.Paginate[MyItemType](paginator)) +func Paginate[I any](p *Paginator) pop.ScopeFunc { + model := pop.Model{Value: *new(I)} + tableName := model.Alias() + return func(q *pop.Query) *pop.Query { + quoteAndContextualize := func(name string) string { + quote := q.Connection.Dialect.Quote + return quote(tableName) + "." + quote(name) + } + dialect := q.Connection.Dialect.Name() + where, args, order := BuildWhereAndOrder(p.PageToken().Columns(), quoteAndContextualize, dialect) + // IMPORTANT: Ensures correct query logic by grouping conditions. + // Without parentheses, `WHERE otherCond AND pageCond1 OR pageCond2` would be + // evaluated as `(otherCond = ? AND pageCond1) OR pageCond2`, potentially returning + // rows that do not match `otherCond`. + // We fix it by forcing the query to be: `WHERE otherCond AND (paginationCond1 OR paginationCond2)`. + where = "(" + where + ")" + + return q. + Where(where, args...). + Order(order). + Limit(p.Size() + 1) + } +} + +func BuildWhereAndOrder(columns []Column, quote func(string) string, dialect string) (string, []any, string) { + var whereBuilder, orderByBuilder, prevEqual strings.Builder + + keysetCols := make([]Column, 0, len(columns)) + + // ORDER BY includes all columns (even constrained ones) + for i, part := range columns { + column := quote(part.Name) + _, keyword := part.Order.extract() + + if i > 0 { + orderByBuilder.WriteString(", ") + } + + orderByBuilder.WriteString(column + " " + keyword) + + // Postgres orders NULLs differently depending on sort direction; + // (ASC → NULLS LAST, DESC → NULLS FIRST), which does not match the + // assumptions of our keyset pagination logic and other supported DBs. + // We therefore make NULL ordering explicit on Postgres (and YugabyteDB, + // which shares Postgres NULL ordering) to keep pagination stable and + // consistent with sqlite/mysql/cockroachdb. + if (dialect == "postgres" || dialect == "yugabyte") && part.Nullable { + if part.Order == OrderAscending { + orderByBuilder.WriteString(" NULLS FIRST") + } else { + orderByBuilder.WriteString(" NULLS LAST") + } + } + + // Build keyset WHERE only from unconstrained columns + if !part.HasConstraint { + keysetCols = append(keysetCols, part) + } + } + + // If everything is constrained, no keyset predicate is needed. + if len(keysetCols) == 0 { + return "", nil, orderByBuilder.String() + } + + args := make([]any, 0, len(keysetCols)*(len(keysetCols)+1)/2) + prevEqualArgs := make([]any, 0, len(keysetCols)) + + whereBuilder.WriteRune('(') + + for i, part := range keysetCols { + column := quote(part.Name) + sign, _ := part.Order.extract() + + if i > 0 { + whereBuilder.WriteString(") OR (") + } + + whereBuilder.WriteString(prevEqual.String()) + if prevEqual.Len() > 0 { + whereBuilder.WriteString(" AND ") + } + + isNull := part.Nullable && isSQLNull(part.Value) + + if !part.Nullable { + whereBuilder.WriteString(column + " " + sign + " ?") + } else if !isNull { + whereBuilder.WriteString(column + " IS NOT NULL AND " + column + " " + sign + " ?") + } else { + whereBuilder.WriteString(column + " IS NOT NULL") + } + + if i > 0 { + prevEqual.WriteString(" AND ") + } + + if !part.Nullable { + prevEqual.WriteString(column + " = ?") + prevEqualArgs = append(prevEqualArgs, part.Value) + } else if !isNull { + prevEqual.WriteString(column + " = ?") + prevEqualArgs = append(prevEqualArgs, part.Value) + } else { + prevEqual.WriteString(column + " IS NULL") + } + + args = append(args, prevEqualArgs...) + } + + whereBuilder.WriteRune(')') + + return whereBuilder.String(), args, orderByBuilder.String() +} + +// isSQLNull reports whether v represents a SQL NULL value. +func isSQLNull(v any) bool { + if v == nil { + return true + } + + if valuer, ok := v.(driver.Valuer); ok { + val, err := valuer.Value() + return err == nil && val == nil + } + + return false +} diff --git a/oryx/pagination/keysetpagination_v2/request_params.go b/oryx/pagination/keysetpagination_v2/request_params.go new file mode 100644 index 0000000..c782349 --- /dev/null +++ b/oryx/pagination/keysetpagination_v2/request_params.go @@ -0,0 +1,126 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package keysetpagination + +import ( + "cmp" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/pkg/errors" +) + +// Pagination Request Parameters +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`) formatted as: +// `; rel="first"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model keysetPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="next" + // + Link string `json:"link"` +} + +// SetLinkHeader adds the Link header for the page encoded by the paginator. +// It contains links to the first and next page, if one exists. +func SetLinkHeader(w http.ResponseWriter, keys [][32]byte, u *url.URL, p *Paginator) { + size := p.Size() + link := []string{linkPart(u, "first", p.DefaultToken().Encrypt(keys), size)} + if !p.isLast { + link = append(link, linkPart(u, "next", p.PageToken().Encrypt(keys), size)) + } + w.Header().Set("Link", strings.Join(link, ",")) +} + +func linkPart(u *url.URL, rel, token string, size int) string { + q := u.Query() + q.Set("page_token", token) + q.Set("page_size", strconv.Itoa(size)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=%q", u.String(), rel) +} + +// ParseQueryParams extracts the pagination options from the URL query. +func ParseQueryParams(keys [][32]byte, q url.Values) ([]Option, error) { + var opts []Option + if t := cmp.Or(q["page_token"]...); t != "" { + raw, err := url.QueryUnescape(t) + if err != nil { + return nil, errors.WithStack(err) + } + token, err := ParsePageToken(keys, raw) + if err != nil { + return nil, err + } + opts = append(opts, WithToken(token)) + } + if s := cmp.Or(q["page_size"]...); s != "" { + size, err := strconv.Atoi(s) + if err != nil { + return nil, errors.WithStack(err) + } + opts = append(opts, WithSize(size)) + } + return opts, nil +} + +// ParsePageToken parses a page token from the given raw string using the provided keys. +// If decryption fails with all provided keys (including when no keys are given), it +// falls back to using the fallbackEncryptionKey and returns an error if that also fails. +func ParsePageToken(keys [][32]byte, raw string) (t PageToken, err error) { + for i := range keys { + err = errors.WithStack(t.decrypt(keys[i], raw)) + if errors.Is(err, ErrInvalidPaginationToken()) { + continue + } + // either we successfully decrypted the token, or we got an error that is not ErrInvalidPaginationToken, in both cases we should return immediately + return t, err + } + // as a last resort, try the fallback key + err = t.decrypt(fallbackEncryptionKey, raw) + return t, errors.WithStack(err) +} diff --git a/oryx/pagination/limit.go b/oryx/pagination/limit.go new file mode 100644 index 0000000..85d80e2 --- /dev/null +++ b/oryx/pagination/limit.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package pagination provides helpers for dealing with pagination. +package pagination + +// Index uses limit, offset, and a slice's length to compute start and end indices for said slice. +func Index(limit, offset, length int) (start, end int) { + if offset > length { + return length, length + } else if limit+offset > length { + return offset, length + } + + return offset, offset + limit +} diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json new file mode 100644 index 0000000..6edb585 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page=1\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json new file mode 100644 index 0000000..e8b6289 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json @@ -0,0 +1,4 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=5\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=5\u003e", + "rel=\"first\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json new file mode 100644 index 0000000..62b1457 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=4\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"prev\",\u003chttp://example.com?page=5\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyNTAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json new file mode 100644 index 0000000..c8797e2 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json @@ -0,0 +1,6 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=4\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"next\",\u003chttp://example.com?page=2\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u0026per_page=50\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json new file mode 100644 index 0000000..d7f3092 --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=50\u003e", + "rel=\"first\",\u003chttp://example.com?page=1\u0026page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u0026per_page=50\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json new file mode 100644 index 0000000..bf1395c --- /dev/null +++ b/oryx/pagination/migrationpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page=0\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u0026per_page=1\u003e", + "rel=\"first\",\u003chttp://example.com?page=21\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIyMSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"next\",\u003chttp://example.com?page=19\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiIxOSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"prev\",\u003chttp://example.com?page=99\u0026page_size=1\u0026page_token=eyJvZmZzZXQiOiI5OSIsInYiOjJ9\u0026per_page=1\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/migrationpagination/header.go b/oryx/pagination/migrationpagination/header.go new file mode 100644 index 0000000..00bb172 --- /dev/null +++ b/oryx/pagination/migrationpagination/header.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrationpagination + +// swagger:model mixedPaginationRequestParameters +type RequestParameters struct { + // Deprecated Items per Page + // + // DEPRECATED: Please use `page_token` instead. This parameter will be removed in the future. + // + // This is the number of items per page. + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PerPage int `json:"per_page"` + + // Deprecated Pagination Page + // + // DEPRECATED: Please use `page_token` instead. This parameter will be removed in the future. + // + // This value is currently an integer, but it is not sequential. The value is not the page number, but a + // reference. The next page can be any number and some numbers might return an empty list. + // + // For example, page 2 might not follow after page 1. And even if page 3 and 5 exist, but page 4 might not exist. + // The first page can be retrieved by omitting this parameter. Following page pointers will be returned in the + // `Link` header. + // + // required: false + // in: query + Page int `json:"page"` + + // Page Size + // + // This is the number of items per page to return. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model mixedPagePaginationResponseHeaders +type ResponseHeaderAnnotation struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. + // + // The header value may look like follows: + // + // ; rel="first",; rel="next",; rel="prev",; rel="last" + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + // + // DEPRECATED: This header will be removed eventually. Please use the `Link` header + // instead to check whether you are on the last page. + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/migrationpagination/pagination.go b/oryx/pagination/migrationpagination/pagination.go new file mode 100644 index 0000000..09073ab --- /dev/null +++ b/oryx/pagination/migrationpagination/pagination.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package migrationpagination + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/ory/x/pagination" + "github.com/ory/x/pagination/pagepagination" + "github.com/ory/x/pagination/tokenpagination" +) + +type Paginator struct { + p *pagepagination.PagePaginator + t *tokenpagination.TokenPaginator +} + +func NewPaginator(p *pagepagination.PagePaginator, t *tokenpagination.TokenPaginator) *Paginator { + return &Paginator{p: p, t: t} +} + +func NewDefaultPaginator() *Paginator { + return &Paginator{p: new(pagepagination.PagePaginator), t: new(tokenpagination.TokenPaginator)} +} + +func (p *Paginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + if r.URL.Query().Has("page_token") || r.URL.Query().Has("page_size") { + return p.t.ParsePagination(r) + } + return p.p.ParsePagination(r) +} + +func header(u *url.URL, rel string, itemsPerPage, offset int64) string { + q := u.Query() + q.Set("page_size", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page_token", tokenpagination.Encode(offset)) + q.Set("per_page", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page", fmt.Sprintf("%d", offset/itemsPerPage)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pagination/pagepagination/header.go b/oryx/pagination/pagepagination/header.go new file mode 100644 index 0000000..22013bf --- /dev/null +++ b/oryx/pagination/pagepagination/header.go @@ -0,0 +1,82 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagepagination + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model pagePaginationRequestParameters +type RequestParameters struct { + // Legacy Items per Page + // + // A DEPRECATED alias for `page_size`. Please transition to using `page_size` going forward. + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 1000 + PerPage int `json:"per_page"` + + // Legacy Pagination Page + // + // A DEPRECATED alias for `page_token`. Please transition to using `page_token` going forward. + // + // required: false + // in: query + // default: 1 + // min: 1 + Page int `json:"page"` + + // Items per Page + // + // This is the number of items per page to return. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. For details on pagination please head over to the + // [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// swagger:model pagePaginationResponseHeaders +type ResponseHeaderAnnotation struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. + // + // This header will include the `per_page` and `page` parameters for legacy reasons, but these parameters will eventually be removed. + // + // Example: Link: ; rel="first",; rel="next",; rel="prev",; rel="last" + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + // + // Example: 123 + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/pagepagination/pagination.go b/oryx/pagination/pagepagination/pagination.go new file mode 100644 index 0000000..a7a3703 --- /dev/null +++ b/oryx/pagination/pagepagination/pagination.go @@ -0,0 +1,79 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagepagination + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/ory/x/pagination" +) + +type PagePaginator struct { + MaxItems int + DefaultItems int +} + +func (p *PagePaginator) defaults() { + if p.MaxItems == 0 { + p.MaxItems = 1000 + } + + if p.DefaultItems == 0 { + p.DefaultItems = 250 + } +} + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func (p *PagePaginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + p.defaults() + + if offsetParam := r.URL.Query().Get("page"); offsetParam == "" { + page = 0 + } else { + if offset, err := strconv.ParseInt(offsetParam, 10, 0); err != nil { + page = 0 + } else { + page = int(offset) + } + } + + if limitParam := r.URL.Query().Get("per_page"); limitParam == "" { + itemsPerPage = p.DefaultItems + } else { + if limit, err := strconv.ParseInt(limitParam, 10, 0); err != nil { + itemsPerPage = p.DefaultItems + } else { + itemsPerPage = int(limit) + } + } + + if itemsPerPage > p.MaxItems { + itemsPerPage = p.MaxItems + } + + if itemsPerPage < 1 { + itemsPerPage = 1 + } + + if page < 0 { + page = 0 + } + + return +} + +func header(u *url.URL, rel string, limit, offset int64) string { + q := u.Query() + q.Set("per_page", fmt.Sprintf("%d", limit)) + q.Set("page", fmt.Sprintf("%d", offset/limit)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pagination/paginationplanner/planner.go b/oryx/pagination/paginationplanner/planner.go new file mode 100644 index 0000000..f148bf0 --- /dev/null +++ b/oryx/pagination/paginationplanner/planner.go @@ -0,0 +1,197 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package paginationplanner + +import ( + "github.com/pkg/errors" + + keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" +) + +// PaginationPlanner chooses the best [PaginationPlan] for a queriedColumns. +// Plan is eligible when its required constraint set is satisfied (and an optional Condition matches). +// If no plan matches, the FallbackPlan is used. +type PaginationPlanner struct { + Plans []PaginationPlan + FallbackPlan PaginationPlan +} + +func NewPaginationPlanner(fallbackPlan PaginationPlan, plans []PaginationPlan) (*PaginationPlanner, error) { + if len(fallbackPlan.DefaultPageToken.Columns()) == 0 { + return nil, errors.New("plan must define at least one PageTokenColumn") + } + + for i := range plans { + plan := &plans[i] + if len(plan.ApplicableQueries) == 0 { + return nil, errors.New("plan must define at least one ApplicableQueries") + } + if len(plan.DefaultPageToken.Columns()) == 0 { + return nil, errors.New("plan must define a DefaultPageToken") + } + plan.populateInternals() + } + + return &PaginationPlanner{ + Plans: plans, + FallbackPlan: fallbackPlan, + }, nil +} + +// GetPaginator selects the first eligible plan for the given queriedColumns constraints. +// Eligibility requires an exact ColumnSet match and (if set) Condition match. +// If none match, the FallbackPlan is used for building the Paginator. +func (p *PaginationPlanner) GetPaginator(q Query, pageOpts ...keysetpagination.Option) (*keysetpagination.Paginator, error) { + if len(q) == 0 { + return keysetpagination.NewPaginator(append(pageOpts, keysetpagination.WithDefaultToken(p.FallbackPlan.DefaultPageToken))...) + } + + plan := p.pickPlan(q) + + origCols := plan.DefaultPageToken.Columns() + + // Make a fresh copy so we don't mutate the original backing array. + cols := make([]keysetpagination.Column, len(origCols)) + copy(cols, origCols) + + for i, col := range cols { + if val, ok := q.colByName(col.Name); ok && val.IsConstrained() { + cols[i].HasConstraint = true + } + } + + defaultToken := keysetpagination.WithDefaultToken(keysetpagination.NewPageToken(cols...)) + return keysetpagination.NewPaginator(append(pageOpts, defaultToken)...) +} + +func (p *PaginationPlanner) pickPlan(q Query) PaginationPlan { + constrainedCols := q.constrainedCols() + + for _, plan := range p.Plans { + if _, ok := plan.applicableQueries[constrainedCols]; !ok { + continue + } + if plan.Condition != nil && !plan.Condition(q) { + continue + } + return plan + } + + return p.FallbackPlan +} + +type PaginationPlan struct { + Name string + + // DefaultPageToken defines the columns that the pagination will be made of + DefaultPageToken keysetpagination.PageToken + + // ApplicableQueries defines the exact sets of columns that this pagination plan is applicable for. + ApplicableQueries [][]Column + applicableQueries map[uint]struct{} + + // Condition further restricts the Plan eligibility for given queriedColumns. + // This can be used for plan-specific logic, e.g. verifying partial-index predicates match. + Condition func(q Query) bool +} + +func (pp *PaginationPlan) populateInternals() { + pp.applicableQueries = make(map[uint]struct{}, len(pp.ApplicableQueries)) + for _, cols := range pp.ApplicableQueries { + var colSet uint + for _, col := range cols { + colSet |= col.bit + } + pp.applicableQueries[colSet] = struct{}{} + } +} + +type Table struct { + nextBit uint + usedNames map[string]struct{} +} + +func NewTable() *Table { + return &Table{ + nextBit: 1, + usedNames: make(map[string]struct{}), + } +} + +func (t *Table) NewColumn(name string) Column { + defer func() { t.nextBit = t.nextBit << 1 }() + if _, exists := t.usedNames[name]; exists { + panic("column name already used: " + name) + } + t.usedNames[name] = struct{}{} + return Column{ + name: name, + bit: t.nextBit, + } +} + +type Column struct { + bit uint + name string +} + +func (c Column) Name() string { + return c.name +} + +type ColumnConstraint uint8 + +const ( + colUnconstrained ColumnConstraint = iota + colConstraintEq // col = ? + colConstraintIsNull // col IS NULL +) + +func (cs ColumnConstraint) IsConstrained() bool { + return cs == colConstraintEq || cs == colConstraintIsNull +} + +func (cs ColumnConstraint) IsNull() bool { + return cs == colConstraintIsNull +} + +// Query is used to track which column constraints are set. +type Query map[Column]ColumnConstraint + +func NewQuery() Query { return make(Query) } + +func (qc Query) SetEq(cols ...Column) Query { + return qc.set(colConstraintEq, cols...) +} + +func (qc Query) SetIsNull(cols ...Column) Query { + return qc.set(colConstraintIsNull, cols...) +} + +func (qc Query) set(c ColumnConstraint, cols ...Column) Query { + for _, col := range cols { + qc[col] = c + } + return qc +} + +func (qc Query) colByName(name string) (ColumnConstraint, bool) { + for col, constraint := range qc { + if col.name == name { + return constraint, true + } + } + return colUnconstrained, false +} + +func (qc Query) constrainedCols() uint { + var cols uint + for col, state := range qc { + if !state.IsConstrained() { + continue + } + cols |= col.bit + } + return cols +} diff --git a/oryx/pagination/parse.go b/oryx/pagination/parse.go new file mode 100644 index 0000000..54f051c --- /dev/null +++ b/oryx/pagination/parse.go @@ -0,0 +1,48 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pagination + +import ( + "net/http" + "strconv" +) + +// Parse parses limit and offset from *http.Request with given limits and defaults. +func Parse(r *http.Request, defaultLimit, defaultOffset, maxLimit int) (int, int) { + var offset, limit int + + if offsetParam := r.URL.Query().Get("offset"); offsetParam == "" { + offset = defaultOffset + } else { + if offset64, err := strconv.ParseInt(offsetParam, 10, 64); err != nil { + offset = defaultOffset + } else { + offset = int(offset64) + } + } + + if limitParam := r.URL.Query().Get("limit"); limitParam == "" { + limit = defaultLimit + } else { + if limit64, err := strconv.ParseInt(limitParam, 10, 64); err != nil { + limit = defaultLimit + } else { + limit = int(limit64) + } + } + + if limit > maxLimit { + limit = maxLimit + } + + if limit < 0 { + limit = 0 + } + + if offset < 0 { + offset = 0 + } + + return limit, offset +} diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json new file mode 100644 index 0000000..2ef4cf1 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_next_and_last,_but_not_previous_or_first_if_at_the_beginning.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json new file mode 100644 index 0000000..77b6783 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_only_first_if_the_limits_provided_exceeds_the_number_of_clients_found.json @@ -0,0 +1,4 @@ +[ + "\u003chttp://example.com?page_size=5\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json new file mode 100644 index 0000000..821898e --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_and_last_if_in_the_middle.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"prev\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyNTAiLCJ2IjoyfQ\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json new file mode 100644 index 0000000..c131e47 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous,_next,_first,_but_not_last_if_in_the_middle_and_no_total_was_provided.json @@ -0,0 +1,6 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIyMDAiLCJ2IjoyfQ\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIxMDAiLCJ2IjoyfQ\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json new file mode 100644 index 0000000..1fb35d5 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Create_previous_and_first_but_not_next_or_last_if_at_the_end.json @@ -0,0 +1,5 @@ +[ + "\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=50\u0026page_token=eyJvZmZzZXQiOiI1MCIsInYiOjJ9\u003e", + "rel=\"prev\"" +] diff --git a/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json new file mode 100644 index 0000000..e5697f0 --- /dev/null +++ b/oryx/pagination/tokenpagination/.snapshots/TestPaginationHeader-Header_should_default_limit_to_1_no_limit_was_provided.json @@ -0,0 +1,7 @@ +[ + "\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIwIiwidiI6Mn0\u003e", + "rel=\"first\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIyMSIsInYiOjJ9\u003e", + "rel=\"next\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiIxOSIsInYiOjJ9\u003e", + "rel=\"prev\",\u003chttp://example.com?page_size=1\u0026page_token=eyJvZmZzZXQiOiI5OSIsInYiOjJ9\u003e", + "rel=\"last\"" +] diff --git a/oryx/pagination/tokenpagination/header.go b/oryx/pagination/tokenpagination/header.go new file mode 100644 index 0000000..180cea1 --- /dev/null +++ b/oryx/pagination/tokenpagination/header.go @@ -0,0 +1,65 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tokenpagination + +// Pagination Request Parameters +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model tokenPaginationRequestParameters +type RequestParameters struct { + // Items per Page + // + // This is the number of items per page to return. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + // default: 250 + // min: 1 + // max: 500 + PageSize int `json:"page_size"` + + // Next Page Token + // + // The next page token. + // For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). + // + // required: false + // in: query + PageToken string `json:"page_token"` +} + +// Pagination Response Header +// +// The `Link` HTTP header contains multiple links (`first`, `next`, `last`, `previous`) formatted as: +// `; rel="{page}"` +// +// For details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination). +// +// swagger:model tokenPaginationResponseHeaders +type ResponseHeaders struct { + // The Link HTTP Header + // + // The `Link` header contains a comma-delimited list of links to the following pages: + // + // - first: The first page of results. + // - next: The next page of results. + // - prev: The previous page of results. + // - last: The last page of results. + // + // Pages are omitted if they do not exist. For example, if there is no next page, the `next` link is omitted. Examples: + // + // ; rel="first",; rel="next",; rel="prev",; rel="last" + // + Link string `json:"link"` + + // The X-Total-Count HTTP Header + // + // The `X-Total-Count` header contains the total number of items in the collection. + TotalCount int `json:"x-total-count"` +} diff --git a/oryx/pagination/tokenpagination/pagination.go b/oryx/pagination/tokenpagination/pagination.go new file mode 100644 index 0000000..8658f86 --- /dev/null +++ b/oryx/pagination/tokenpagination/pagination.go @@ -0,0 +1,93 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tokenpagination + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/pkg/errors" + "github.com/tidwall/gjson" + + "github.com/ory/x/pagination" + + "github.com/ory/herodot" +) + +func Encode(offset int64) string { + return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf(`{"offset":"%d","v":2}`, offset))) +} + +func decode(s string) (int, error) { + b, err := base64.RawURLEncoding.DecodeString(s) + if err != nil { + return 0, errors.WithStack(herodot.ErrBadRequest().WithWrap(err).WithReasonf("Unable to parse pagination token: %s", err)) + } + + return int(gjson.Get(string(b), "offset").Int()), nil +} + +type TokenPaginator struct { + MaxItems int + DefaultItems int +} + +func (p *TokenPaginator) defaults() { + if p.MaxItems == 0 { + p.MaxItems = 1000 + } + + if p.DefaultItems == 0 { + p.DefaultItems = 250 + } +} + +// ParsePagination parses limit and page from *http.Request with given limits and defaults. +func (p *TokenPaginator) ParsePagination(r *http.Request) (page, itemsPerPage int) { + p.defaults() + + var offset int + if offsetParam := r.URL.Query().Get("page_token"); len(offsetParam) > 0 { + offset, _ = decode(offsetParam) + } + + if gotLimit, err := strconv.ParseInt(r.URL.Query().Get("page_size"), 10, 0); err == nil { + itemsPerPage = int(gotLimit) + } else { + itemsPerPage = p.DefaultItems + } + + if itemsPerPage > p.MaxItems { + itemsPerPage = p.MaxItems + } + + if itemsPerPage < 1 { + itemsPerPage = 1 + } + + if offset > 0 { + page = offset / itemsPerPage + } + + if page < 0 { + page = 0 + } + + return +} + +func header(u *url.URL, rel string, itemsPerPage, offset int64) string { + q := u.Query() + q.Set("page_size", fmt.Sprintf("%d", itemsPerPage)) + q.Set("page_token", Encode(offset)) + u.RawQuery = q.Encode() + return fmt.Sprintf("<%s>; rel=\"%s\"", u.String(), rel) +} + +func PaginationHeader(w http.ResponseWriter, u *url.URL, total int64, page, itemsPerPage int) { + pagination.HeaderWithFormatter(w, u, total, page, itemsPerPage, header) +} diff --git a/oryx/pointerx/pointerx.go b/oryx/pointerx/pointerx.go new file mode 100644 index 0000000..0f6bec5 --- /dev/null +++ b/oryx/pointerx/pointerx.go @@ -0,0 +1,13 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package pointerx + +// Deref returns the input values de-referenced value, or zero value if nil. +func Deref[T any](p *T) T { + if p == nil { + var zero T + return zero + } + return *p +} diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt new file mode 100644 index 0000000..9ca2705 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-final_status.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt new file mode 100644 index 0000000..96811ff --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_do_not_confirm.txt @@ -0,0 +1,225 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Rollback +20200831110752000019 identity_verifiable_address_remove_code Rollback +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL + +Do you wish to execute this migration plan? [y/n]: ------------ WARNING ------------ +Migration aborted. + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt new file mode 100644 index 0000000..fb2bb5e --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_but_no_steps.txt @@ -0,0 +1,217 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: +There are apparently no migrations to roll back. +Please provide the --steps argument with a value larger than 0. + + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt new file mode 100644 index 0000000..112d130 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_four_steps.txt @@ -0,0 +1,230 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Rollback +20200831110752000021 identity_verifiable_address_remove_code Rollback +20201201161451000000 credential_types_values Rollback +20201201161451000001 credential_types_values Rollback + +The SQL statements to be executed from top to bottom are: + +------------ 20201201161451000001 - credential_types_values ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME + +------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt new file mode 100644 index 0000000..dda2da5 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_down_two_steps.txt @@ -0,0 +1,225 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Rollback +20200831110752000019 identity_verifiable_address_remove_code Rollback +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL + +Do you wish to execute this migration plan? [y/n]: ------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt new file mode 100644 index 0000000..e066817 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_again.txt @@ -0,0 +1,237 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') + +------------ 20201201161451000001 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); + +Do you wish to execute this migration plan? [y/n]: ------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt new file mode 100644 index 0000000..6d297e3 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_rollbacks_up_without_confirm.txt @@ -0,0 +1,219 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +The SQL statements to be executed from top to bottom are: + +Do you wish to execute this migration plan? [y/n]: ------------ WARNING ------------ +Migration aborted. + +stderr: To skip the next question use flag --yes (at your own risk). + diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt new file mode 100644 index 0000000..ad1f3de --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-migrate_up.txt @@ -0,0 +1,1172 @@ +stdout: The migration plan is as follows: +Version Name Status +20191100000001000000 identities Pending +20191100000001000001 identities Pending +20191100000001000002 identities Pending +20191100000001000003 identities Pending +20191100000001000004 identities Pending +20191100000001000005 identities Pending +20191100000002000000 requests Pending +20191100000002000001 requests Pending +20191100000002000002 requests Pending +20191100000002000003 requests Pending +20191100000002000004 requests Pending +20191100000003000000 sessions Pending +20191100000004000000 errors Pending +20191100000006000000 courier Pending +20191100000007000000 errors Pending +20191100000007000001 errors Pending +20191100000007000002 errors Pending +20191100000007000003 errors Pending +20191100000008000000 selfservice_verification Pending +20191100000008000001 selfservice_verification Pending +20191100000008000002 selfservice_verification Pending +20191100000008000003 selfservice_verification Pending +20191100000008000004 selfservice_verification Pending +20191100000008000005 selfservice_verification Pending +20191100000010000000 errors Pending +20191100000010000001 errors Pending +20191100000010000002 errors Pending +20191100000010000003 errors Pending +20191100000010000004 errors Pending +20191100000011000000 courier_body_type Pending +20191100000011000001 courier_body_type Pending +20191100000011000002 courier_body_type Pending +20191100000011000003 courier_body_type Pending +20191100000012000000 login_request_forced Pending +20191100000012000001 login_request_forced Pending +20191100000012000002 login_request_forced Pending +20191100000012000003 login_request_forced Pending +20200317160354000000 create_profile_request_forms Pending +20200317160354000001 create_profile_request_forms Pending +20200317160354000002 create_profile_request_forms Pending +20200317160354000003 create_profile_request_forms Pending +20200317160354000004 create_profile_request_forms Pending +20200317160354000005 create_profile_request_forms Pending +20200317160354000006 create_profile_request_forms Pending +20200401183443000000 continuity_containers Pending +20200402142539000000 rename_profile_flows Pending +20200402142539000001 rename_profile_flows Pending +20200402142539000002 rename_profile_flows Pending +20200519101057000000 create_recovery_addresses Pending +20200519101057000001 create_recovery_addresses Pending +20200519101057000002 create_recovery_addresses Pending +20200519101057000003 create_recovery_addresses Pending +20200519101057000004 create_recovery_addresses Pending +20200519101057000005 create_recovery_addresses Pending +20200519101057000006 create_recovery_addresses Pending +20200519101057000007 create_recovery_addresses Pending +20200601101000000000 create_messages Pending +20200601101000000001 create_messages Pending +20200601101000000002 create_messages Pending +20200601101000000003 create_messages Pending +20200605111551000000 messages Pending +20200605111551000001 messages Pending +20200605111551000002 messages Pending +20200605111551000003 messages Pending +20200605111551000004 messages Pending +20200605111551000005 messages Pending +20200605111551000006 messages Pending +20200605111551000007 messages Pending +20200605111551000008 messages Pending +20200605111551000009 messages Pending +20200605111551000010 messages Pending +20200605111551000011 messages Pending +20200607165100000000 settings Pending +20200607165100000001 settings Pending +20200607165100000002 settings Pending +20200607165100000003 settings Pending +20200607165100000004 settings Pending +20200705105359000000 rename_identities_schema Pending +20200810141652000000 flow_type Pending +20200810141652000001 flow_type Pending +20200810141652000002 flow_type Pending +20200810141652000003 flow_type Pending +20200810141652000004 flow_type Pending +20200810141652000005 flow_type Pending +20200810141652000006 flow_type Pending +20200810141652000007 flow_type Pending +20200810141652000008 flow_type Pending +20200810141652000009 flow_type Pending +20200810141652000010 flow_type Pending +20200810141652000011 flow_type Pending +20200810141652000012 flow_type Pending +20200810141652000013 flow_type Pending +20200810141652000014 flow_type Pending +20200810141652000015 flow_type Pending +20200810141652000016 flow_type Pending +20200810141652000017 flow_type Pending +20200810141652000018 flow_type Pending +20200810141652000019 flow_type Pending +20200810161022000000 flow_rename Pending +20200810161022000001 flow_rename Pending +20200810161022000002 flow_rename Pending +20200810161022000003 flow_rename Pending +20200810161022000004 flow_rename Pending +20200810161022000005 flow_rename Pending +20200810161022000006 flow_rename Pending +20200810161022000007 flow_rename Pending +20200810161022000008 flow_rename Pending +20200810162450000000 flow_fields_rename Pending +20200810162450000001 flow_fields_rename Pending +20200810162450000002 flow_fields_rename Pending +20200810162450000003 flow_fields_rename Pending +20200812124254000000 add_session_token Pending +20200812124254000001 add_session_token Pending +20200812124254000002 add_session_token Pending +20200812124254000003 add_session_token Pending +20200812124254000004 add_session_token Pending +20200812124254000005 add_session_token Pending +20200812124254000006 add_session_token Pending +20200812124254000007 add_session_token Pending +20200812160551000000 add_session_revoke Pending +20200812160551000001 add_session_revoke Pending +20200812160551000002 add_session_revoke Pending +20200812160551000003 add_session_revoke Pending +20200812160551000004 add_session_revoke Pending +20200812160551000005 add_session_revoke Pending +20200812160551000006 add_session_revoke Pending +20200812160551000007 add_session_revoke Pending +20200830121710000000 update_recovery_token Pending +20200830130642000000 add_verification_methods Pending +20200830130642000001 add_verification_methods Pending +20200830130642000002 add_verification_methods Pending +20200830130642000003 add_verification_methods Pending +20200830130642000004 add_verification_methods Pending +20200830130642000005 add_verification_methods Pending +20200830130642000006 add_verification_methods Pending +20200830130642000007 add_verification_methods Pending +20200830130642000008 add_verification_methods Pending +20200830130642000009 add_verification_methods Pending +20200830130642000010 add_verification_methods Pending +20200830130643000000 add_verification_methods Pending +20200830130644000000 add_verification_methods Pending +20200830130644000001 add_verification_methods Pending +20200830130645000000 add_verification_methods Pending +20200830130646000000 add_verification_methods Pending +20200830130646000001 add_verification_methods Pending +20200830130646000002 add_verification_methods Pending +20200830130646000003 add_verification_methods Pending +20200830130646000004 add_verification_methods Pending +20200830130646000005 add_verification_methods Pending +20200830130646000006 add_verification_methods Pending +20200830130646000007 add_verification_methods Pending +20200830130646000008 add_verification_methods Pending +20200830130646000009 add_verification_methods Pending +20200830130646000010 add_verification_methods Pending +20200830130646000011 add_verification_methods Pending +20200830154602000000 add_verification_token Pending +20200830154602000001 add_verification_token Pending +20200830154602000002 add_verification_token Pending +20200830154602000003 add_verification_token Pending +20200830154602000004 add_verification_token Pending +20200830172221000000 recovery_token_expires Pending +20200830172221000001 recovery_token_expires Pending +20200830172221000002 recovery_token_expires Pending +20200830172221000003 recovery_token_expires Pending +20200830172221000004 recovery_token_expires Pending +20200830172221000005 recovery_token_expires Pending +20200830172221000006 recovery_token_expires Pending +20200830172221000007 recovery_token_expires Pending +20200830172221000008 recovery_token_expires Pending +20200830172221000009 recovery_token_expires Pending +20200830172221000010 recovery_token_expires Pending +20200830172221000011 recovery_token_expires Pending +20200830172221000012 recovery_token_expires Pending +20200830172221000013 recovery_token_expires Pending +20200830172221000014 recovery_token_expires Pending +20200830172221000015 recovery_token_expires Pending +20200830172221000016 recovery_token_expires Pending +20200830172221000017 recovery_token_expires Pending +20200830172221000018 recovery_token_expires Pending +20200830172221000019 recovery_token_expires Pending +20200830172221000020 recovery_token_expires Pending +20200830172221000021 recovery_token_expires Pending +20200830172221000022 recovery_token_expires Pending +20200830172221000023 recovery_token_expires Pending +20200830172221000024 recovery_token_expires Pending +20200831110752000000 identity_verifiable_address_remove_code Pending +20200831110752000001 identity_verifiable_address_remove_code Pending +20200831110752000002 identity_verifiable_address_remove_code Pending +20200831110752000003 identity_verifiable_address_remove_code Pending +20200831110752000004 identity_verifiable_address_remove_code Pending +20200831110752000005 identity_verifiable_address_remove_code Pending +20200831110752000006 identity_verifiable_address_remove_code Pending +20200831110752000007 identity_verifiable_address_remove_code Pending +20200831110752000008 identity_verifiable_address_remove_code Pending +20200831110752000009 identity_verifiable_address_remove_code Pending +20200831110752000010 identity_verifiable_address_remove_code Pending +20200831110752000011 identity_verifiable_address_remove_code Pending +20200831110752000012 identity_verifiable_address_remove_code Pending +20200831110752000013 identity_verifiable_address_remove_code Pending +20200831110752000014 identity_verifiable_address_remove_code Pending +20200831110752000015 identity_verifiable_address_remove_code Pending +20200831110752000016 identity_verifiable_address_remove_code Pending +20200831110752000017 identity_verifiable_address_remove_code Pending +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +The SQL statements to be executed from top to bottom are: + +------------ 20191100000001000000 - identities ------------ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000001000001 - identities ------------ +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +) + +------------ 20191100000001000002 - identities ------------ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) + +------------ 20191100000001000003 - identities ------------ +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +) + +------------ 20191100000001000004 - identities ------------ +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +) + +------------ 20191100000001000005 - identities ------------ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); + +------------ 20191100000002000000 - requests ------------ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000002000001 - requests ------------ +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +) + +------------ 20191100000002000002 - requests ------------ +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000002000003 - requests ------------ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +) + +------------ 20191100000002000004 - requests ------------ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20191100000003000000 - sessions ------------ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20191100000004000000 - errors ------------ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000006000000 - courier ------------ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000007000000 - errors ------------ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; + +------------ 20191100000007000001 - errors ------------ + + +------------ 20191100000007000002 - errors ------------ + + +------------ 20191100000007000003 - errors ------------ + + +------------ 20191100000008000000 - selfservice_verification ------------ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20191100000008000001 - selfservice_verification ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) + +------------ 20191100000008000002 - selfservice_verification ------------ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) + +------------ 20191100000008000003 - selfservice_verification ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) + +------------ 20191100000008000004 - selfservice_verification ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) + +------------ 20191100000008000005 - selfservice_verification ------------ +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); + +------------ 20191100000010000000 - errors ------------ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) + +------------ 20191100000010000001 - errors ------------ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" + +------------ 20191100000010000002 - errors ------------ +DROP TABLE "selfservice_errors" + +------------ 20191100000010000003 - errors ------------ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; + +------------ 20191100000010000004 - errors ------------ + + +------------ 20191100000011000000 - courier_body_type ------------ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20191100000011000001 - courier_body_type ------------ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages" + +------------ 20191100000011000002 - courier_body_type ------------ +DROP TABLE "courier_messages" + +------------ 20191100000011000003 - courier_body_type ------------ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; + +------------ 20191100000012000000 - login_request_forced ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; + +------------ 20191100000012000001 - login_request_forced ------------ + + +------------ 20191100000012000002 - login_request_forced ------------ + + +------------ 20191100000012000003 - login_request_forced ------------ + + +------------ 20200317160354000000 - create_profile_request_forms ------------ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20200317160354000001 - create_profile_request_forms ------------ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT + +------------ 20200317160354000002 - create_profile_request_forms ------------ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests + +------------ 20200317160354000003 - create_profile_request_forms ------------ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200317160354000004 - create_profile_request_forms ------------ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests" + +------------ 20200317160354000005 - create_profile_request_forms ------------ + +DROP TABLE "selfservice_profile_management_requests" + +------------ 20200317160354000006 - create_profile_request_forms ------------ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; + +------------ 20200401183443000000 - continuity_containers ------------ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); + +------------ 20200402142539000000 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" + +------------ 20200402142539000001 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" + +------------ 20200402142539000002 - rename_profile_flows ------------ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; + +------------ 20200519101057000000 - create_recovery_addresses ------------ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20200519101057000001 - create_recovery_addresses ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) + +------------ 20200519101057000002 - create_recovery_addresses ------------ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) + +------------ 20200519101057000003 - create_recovery_addresses ------------ +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +) + +------------ 20200519101057000004 - create_recovery_addresses ------------ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) + +------------ 20200519101057000005 - create_recovery_addresses ------------ +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) + +------------ 20200519101057000006 - create_recovery_addresses ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) + +------------ 20200519101057000007 - create_recovery_addresses ------------ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); + +------------ 20200601101000000000 - create_messages ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; + +------------ 20200601101000000001 - create_messages ------------ + + +------------ 20200601101000000002 - create_messages ------------ + + +------------ 20200601101000000003 - create_messages ------------ + + +------------ 20200605111551000000 - messages ------------ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT + +------------ 20200605111551000001 - messages ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT + +------------ 20200605111551000002 - messages ------------ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; + +------------ 20200605111551000003 - messages ------------ + + +------------ 20200605111551000004 - messages ------------ + + +------------ 20200605111551000005 - messages ------------ + + +------------ 20200605111551000006 - messages ------------ + + +------------ 20200605111551000007 - messages ------------ + + +------------ 20200605111551000008 - messages ------------ + + +------------ 20200605111551000009 - messages ------------ + + +------------ 20200605111551000010 - messages ------------ + + +------------ 20200605111551000011 - messages ------------ + + +------------ 20200607165100000000 - settings ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form' + +------------ 20200607165100000001 - settings ------------ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200607165100000002 - settings ------------ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" + +------------ 20200607165100000003 - settings ------------ + +DROP TABLE "selfservice_settings_requests" + +------------ 20200607165100000004 - settings ------------ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; + +------------ 20200705105359000000 - rename_identities_schema ------------ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; + +------------ 20200810141652000000 - flow_type ------------ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000001 - flow_type ------------ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000002 - flow_type ------------ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000003 - flow_type ------------ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' + +------------ 20200810141652000004 - flow_type ------------ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; + +------------ 20200810141652000005 - flow_type ------------ + + +------------ 20200810141652000006 - flow_type ------------ + + +------------ 20200810141652000007 - flow_type ------------ + + +------------ 20200810141652000008 - flow_type ------------ + + +------------ 20200810141652000009 - flow_type ------------ + + +------------ 20200810141652000010 - flow_type ------------ + + +------------ 20200810141652000011 - flow_type ------------ + + +------------ 20200810141652000012 - flow_type ------------ + + +------------ 20200810141652000013 - flow_type ------------ + + +------------ 20200810141652000014 - flow_type ------------ + + +------------ 20200810141652000015 - flow_type ------------ + + +------------ 20200810141652000016 - flow_type ------------ + + +------------ 20200810141652000017 - flow_type ------------ + + +------------ 20200810141652000018 - flow_type ------------ + + +------------ 20200810141652000019 - flow_type ------------ + + +------------ 20200810161022000000 - flow_rename ------------ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" + +------------ 20200810161022000001 - flow_rename ------------ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" + +------------ 20200810161022000002 - flow_rename ------------ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" + +------------ 20200810161022000003 - flow_rename ------------ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" + +------------ 20200810161022000004 - flow_rename ------------ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" + +------------ 20200810161022000005 - flow_rename ------------ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" + +------------ 20200810161022000006 - flow_rename ------------ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" + +------------ 20200810161022000007 - flow_rename ------------ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" + +------------ 20200810161022000008 - flow_rename ------------ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; + +------------ 20200810162450000000 - flow_fields_rename ------------ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" + +------------ 20200810162450000001 - flow_fields_rename ------------ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" + +------------ 20200810162450000002 - flow_fields_rename ------------ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" + +------------ 20200810162450000003 - flow_fields_rename ------------ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; + +------------ 20200812124254000000 - add_session_token ------------ +DELETE FROM sessions + +------------ 20200812124254000001 - add_session_token ------------ +ALTER TABLE "sessions" ADD COLUMN "token" TEXT + +------------ 20200812124254000002 - add_session_token ------------ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200812124254000003 - add_session_token ------------ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" + +------------ 20200812124254000004 - add_session_token ------------ +DROP TABLE "sessions" + +------------ 20200812124254000005 - add_session_token ------------ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions" + +------------ 20200812124254000006 - add_session_token ------------ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) + +------------ 20200812124254000007 - add_session_token ------------ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); + +------------ 20200812160551000000 - add_session_revoke ------------ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; + +------------ 20200812160551000001 - add_session_revoke ------------ + + +------------ 20200812160551000002 - add_session_revoke ------------ + + +------------ 20200812160551000003 - add_session_revoke ------------ + + +------------ 20200812160551000004 - add_session_revoke ------------ + + +------------ 20200812160551000005 - add_session_revoke ------------ + + +------------ 20200812160551000006 - add_session_revoke ------------ + + +------------ 20200812160551000007 - add_session_revoke ------------ + + +------------ 20200830121710000000 - update_recovery_token ------------ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; + +------------ 20200830130642000000 - add_verification_methods ------------ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; + +------------ 20200830130642000001 - add_verification_methods ------------ + + +------------ 20200830130642000002 - add_verification_methods ------------ + + +------------ 20200830130642000003 - add_verification_methods ------------ + + +------------ 20200830130642000004 - add_verification_methods ------------ + + +------------ 20200830130642000005 - add_verification_methods ------------ + + +------------ 20200830130642000006 - add_verification_methods ------------ + + +------------ 20200830130642000007 - add_verification_methods ------------ + + +------------ 20200830130642000008 - add_verification_methods ------------ + + +------------ 20200830130642000009 - add_verification_methods ------------ + + +------------ 20200830130642000010 - add_verification_methods ------------ + + +------------ 20200830130643000000 - add_verification_methods ------------ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; + +------------ 20200830130644000000 - add_verification_methods ------------ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) + +------------ 20200830130644000001 - add_verification_methods ------------ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; + +------------ 20200830130645000000 - add_verification_methods ------------ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; + +------------ 20200830130646000000 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000001 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000002 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000003 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" + +------------ 20200830130646000004 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000005 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000006 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000007 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" + +------------ 20200830130646000008 - add_verification_methods ------------ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) + +------------ 20200830130646000009 - add_verification_methods ------------ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" + +------------ 20200830130646000010 - add_verification_methods ------------ + +DROP TABLE "selfservice_verification_flows" + +------------ 20200830130646000011 - add_verification_methods ------------ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; + +------------ 20200830154602000000 - add_verification_token ------------ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +) + +------------ 20200830154602000001 - add_verification_token ------------ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) + +------------ 20200830154602000002 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) + +------------ 20200830154602000003 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) + +------------ 20200830154602000004 - add_verification_token ------------ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); + +------------ 20200830172221000000 - recovery_token_expires ------------ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' + +------------ 20200830172221000001 - recovery_token_expires ------------ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' + +------------ 20200830172221000002 - recovery_token_expires ------------ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" + +------------ 20200830172221000003 - recovery_token_expires ------------ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" + +------------ 20200830172221000004 - recovery_token_expires ------------ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200830172221000005 - recovery_token_expires ------------ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) + +------------ 20200830172221000006 - recovery_token_expires ------------ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) + +------------ 20200830172221000007 - recovery_token_expires ------------ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" + +------------ 20200830172221000008 - recovery_token_expires ------------ +DROP TABLE "identity_recovery_tokens" + +------------ 20200830172221000009 - recovery_token_expires ------------ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; + +------------ 20200830172221000010 - recovery_token_expires ------------ + + +------------ 20200830172221000011 - recovery_token_expires ------------ + + +------------ 20200830172221000012 - recovery_token_expires ------------ + + +------------ 20200830172221000013 - recovery_token_expires ------------ + + +------------ 20200830172221000014 - recovery_token_expires ------------ + + +------------ 20200830172221000015 - recovery_token_expires ------------ + + +------------ 20200830172221000016 - recovery_token_expires ------------ + + +------------ 20200830172221000017 - recovery_token_expires ------------ + + +------------ 20200830172221000018 - recovery_token_expires ------------ + + +------------ 20200830172221000019 - recovery_token_expires ------------ + + +------------ 20200830172221000020 - recovery_token_expires ------------ + + +------------ 20200830172221000021 - recovery_token_expires ------------ + + +------------ 20200830172221000022 - recovery_token_expires ------------ + + +------------ 20200830172221000023 - recovery_token_expires ------------ + + +------------ 20200830172221000024 - recovery_token_expires ------------ + + +------------ 20200831110752000000 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" + +------------ 20200831110752000001 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" + +------------ 20200831110752000002 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" + +------------ 20200831110752000003 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" + +------------ 20200831110752000004 - identity_verifiable_address_remove_code ------------ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200831110752000005 - identity_verifiable_address_remove_code ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000006 - identity_verifiable_address_remove_code ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000007 - identity_verifiable_address_remove_code ------------ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" + +------------ 20200831110752000008 - identity_verifiable_address_remove_code ------------ + +DROP TABLE "identity_verifiable_addresses" + +------------ 20200831110752000009 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" + +------------ 20200831110752000010 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" + +------------ 20200831110752000011 - identity_verifiable_address_remove_code ------------ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" + +------------ 20200831110752000012 - identity_verifiable_address_remove_code ------------ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) + +------------ 20200831110752000013 - identity_verifiable_address_remove_code ------------ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000014 - identity_verifiable_address_remove_code ------------ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) + +------------ 20200831110752000015 - identity_verifiable_address_remove_code ------------ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" + +------------ 20200831110752000016 - identity_verifiable_address_remove_code ------------ + +DROP TABLE "identity_verifiable_addresses" + +------------ 20200831110752000017 - identity_verifiable_address_remove_code ------------ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; + +------------ 20200831110752000018 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000019 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000020 - identity_verifiable_address_remove_code ------------ + + +------------ 20200831110752000021 - identity_verifiable_address_remove_code ------------ + + +------------ 20201201161451000000 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') + +------------ 20201201161451000001 - credential_types_values ------------ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); + +------------ SUCCESS ------------ +Successfully applied migrations! + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt new file mode 100644 index 0000000..9ca2705 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_migrated.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Applied +20200831110752000021 identity_verifiable_address_remove_code Applied +20201201161451000000 credential_types_values Applied +20201201161451000001 credential_types_values Applied + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt new file mode 100644 index 0000000..1f2f2fc --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_pre.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Pending +20191100000001000001 identities Pending +20191100000001000002 identities Pending +20191100000001000003 identities Pending +20191100000001000004 identities Pending +20191100000001000005 identities Pending +20191100000002000000 requests Pending +20191100000002000001 requests Pending +20191100000002000002 requests Pending +20191100000002000003 requests Pending +20191100000002000004 requests Pending +20191100000003000000 sessions Pending +20191100000004000000 errors Pending +20191100000006000000 courier Pending +20191100000007000000 errors Pending +20191100000007000001 errors Pending +20191100000007000002 errors Pending +20191100000007000003 errors Pending +20191100000008000000 selfservice_verification Pending +20191100000008000001 selfservice_verification Pending +20191100000008000002 selfservice_verification Pending +20191100000008000003 selfservice_verification Pending +20191100000008000004 selfservice_verification Pending +20191100000008000005 selfservice_verification Pending +20191100000010000000 errors Pending +20191100000010000001 errors Pending +20191100000010000002 errors Pending +20191100000010000003 errors Pending +20191100000010000004 errors Pending +20191100000011000000 courier_body_type Pending +20191100000011000001 courier_body_type Pending +20191100000011000002 courier_body_type Pending +20191100000011000003 courier_body_type Pending +20191100000012000000 login_request_forced Pending +20191100000012000001 login_request_forced Pending +20191100000012000002 login_request_forced Pending +20191100000012000003 login_request_forced Pending +20200317160354000000 create_profile_request_forms Pending +20200317160354000001 create_profile_request_forms Pending +20200317160354000002 create_profile_request_forms Pending +20200317160354000003 create_profile_request_forms Pending +20200317160354000004 create_profile_request_forms Pending +20200317160354000005 create_profile_request_forms Pending +20200317160354000006 create_profile_request_forms Pending +20200401183443000000 continuity_containers Pending +20200402142539000000 rename_profile_flows Pending +20200402142539000001 rename_profile_flows Pending +20200402142539000002 rename_profile_flows Pending +20200519101057000000 create_recovery_addresses Pending +20200519101057000001 create_recovery_addresses Pending +20200519101057000002 create_recovery_addresses Pending +20200519101057000003 create_recovery_addresses Pending +20200519101057000004 create_recovery_addresses Pending +20200519101057000005 create_recovery_addresses Pending +20200519101057000006 create_recovery_addresses Pending +20200519101057000007 create_recovery_addresses Pending +20200601101000000000 create_messages Pending +20200601101000000001 create_messages Pending +20200601101000000002 create_messages Pending +20200601101000000003 create_messages Pending +20200605111551000000 messages Pending +20200605111551000001 messages Pending +20200605111551000002 messages Pending +20200605111551000003 messages Pending +20200605111551000004 messages Pending +20200605111551000005 messages Pending +20200605111551000006 messages Pending +20200605111551000007 messages Pending +20200605111551000008 messages Pending +20200605111551000009 messages Pending +20200605111551000010 messages Pending +20200605111551000011 messages Pending +20200607165100000000 settings Pending +20200607165100000001 settings Pending +20200607165100000002 settings Pending +20200607165100000003 settings Pending +20200607165100000004 settings Pending +20200705105359000000 rename_identities_schema Pending +20200810141652000000 flow_type Pending +20200810141652000001 flow_type Pending +20200810141652000002 flow_type Pending +20200810141652000003 flow_type Pending +20200810141652000004 flow_type Pending +20200810141652000005 flow_type Pending +20200810141652000006 flow_type Pending +20200810141652000007 flow_type Pending +20200810141652000008 flow_type Pending +20200810141652000009 flow_type Pending +20200810141652000010 flow_type Pending +20200810141652000011 flow_type Pending +20200810141652000012 flow_type Pending +20200810141652000013 flow_type Pending +20200810141652000014 flow_type Pending +20200810141652000015 flow_type Pending +20200810141652000016 flow_type Pending +20200810141652000017 flow_type Pending +20200810141652000018 flow_type Pending +20200810141652000019 flow_type Pending +20200810161022000000 flow_rename Pending +20200810161022000001 flow_rename Pending +20200810161022000002 flow_rename Pending +20200810161022000003 flow_rename Pending +20200810161022000004 flow_rename Pending +20200810161022000005 flow_rename Pending +20200810161022000006 flow_rename Pending +20200810161022000007 flow_rename Pending +20200810161022000008 flow_rename Pending +20200810162450000000 flow_fields_rename Pending +20200810162450000001 flow_fields_rename Pending +20200810162450000002 flow_fields_rename Pending +20200810162450000003 flow_fields_rename Pending +20200812124254000000 add_session_token Pending +20200812124254000001 add_session_token Pending +20200812124254000002 add_session_token Pending +20200812124254000003 add_session_token Pending +20200812124254000004 add_session_token Pending +20200812124254000005 add_session_token Pending +20200812124254000006 add_session_token Pending +20200812124254000007 add_session_token Pending +20200812160551000000 add_session_revoke Pending +20200812160551000001 add_session_revoke Pending +20200812160551000002 add_session_revoke Pending +20200812160551000003 add_session_revoke Pending +20200812160551000004 add_session_revoke Pending +20200812160551000005 add_session_revoke Pending +20200812160551000006 add_session_revoke Pending +20200812160551000007 add_session_revoke Pending +20200830121710000000 update_recovery_token Pending +20200830130642000000 add_verification_methods Pending +20200830130642000001 add_verification_methods Pending +20200830130642000002 add_verification_methods Pending +20200830130642000003 add_verification_methods Pending +20200830130642000004 add_verification_methods Pending +20200830130642000005 add_verification_methods Pending +20200830130642000006 add_verification_methods Pending +20200830130642000007 add_verification_methods Pending +20200830130642000008 add_verification_methods Pending +20200830130642000009 add_verification_methods Pending +20200830130642000010 add_verification_methods Pending +20200830130643000000 add_verification_methods Pending +20200830130644000000 add_verification_methods Pending +20200830130644000001 add_verification_methods Pending +20200830130645000000 add_verification_methods Pending +20200830130646000000 add_verification_methods Pending +20200830130646000001 add_verification_methods Pending +20200830130646000002 add_verification_methods Pending +20200830130646000003 add_verification_methods Pending +20200830130646000004 add_verification_methods Pending +20200830130646000005 add_verification_methods Pending +20200830130646000006 add_verification_methods Pending +20200830130646000007 add_verification_methods Pending +20200830130646000008 add_verification_methods Pending +20200830130646000009 add_verification_methods Pending +20200830130646000010 add_verification_methods Pending +20200830130646000011 add_verification_methods Pending +20200830154602000000 add_verification_token Pending +20200830154602000001 add_verification_token Pending +20200830154602000002 add_verification_token Pending +20200830154602000003 add_verification_token Pending +20200830154602000004 add_verification_token Pending +20200830172221000000 recovery_token_expires Pending +20200830172221000001 recovery_token_expires Pending +20200830172221000002 recovery_token_expires Pending +20200830172221000003 recovery_token_expires Pending +20200830172221000004 recovery_token_expires Pending +20200830172221000005 recovery_token_expires Pending +20200830172221000006 recovery_token_expires Pending +20200830172221000007 recovery_token_expires Pending +20200830172221000008 recovery_token_expires Pending +20200830172221000009 recovery_token_expires Pending +20200830172221000010 recovery_token_expires Pending +20200830172221000011 recovery_token_expires Pending +20200830172221000012 recovery_token_expires Pending +20200830172221000013 recovery_token_expires Pending +20200830172221000014 recovery_token_expires Pending +20200830172221000015 recovery_token_expires Pending +20200830172221000016 recovery_token_expires Pending +20200830172221000017 recovery_token_expires Pending +20200830172221000018 recovery_token_expires Pending +20200830172221000019 recovery_token_expires Pending +20200830172221000020 recovery_token_expires Pending +20200830172221000021 recovery_token_expires Pending +20200830172221000022 recovery_token_expires Pending +20200830172221000023 recovery_token_expires Pending +20200830172221000024 recovery_token_expires Pending +20200831110752000000 identity_verifiable_address_remove_code Pending +20200831110752000001 identity_verifiable_address_remove_code Pending +20200831110752000002 identity_verifiable_address_remove_code Pending +20200831110752000003 identity_verifiable_address_remove_code Pending +20200831110752000004 identity_verifiable_address_remove_code Pending +20200831110752000005 identity_verifiable_address_remove_code Pending +20200831110752000006 identity_verifiable_address_remove_code Pending +20200831110752000007 identity_verifiable_address_remove_code Pending +20200831110752000008 identity_verifiable_address_remove_code Pending +20200831110752000009 identity_verifiable_address_remove_code Pending +20200831110752000010 identity_verifiable_address_remove_code Pending +20200831110752000011 identity_verifiable_address_remove_code Pending +20200831110752000012 identity_verifiable_address_remove_code Pending +20200831110752000013 identity_verifiable_address_remove_code Pending +20200831110752000014 identity_verifiable_address_remove_code Pending +20200831110752000015 identity_verifiable_address_remove_code Pending +20200831110752000016 identity_verifiable_address_remove_code Pending +20200831110752000017 identity_verifiable_address_remove_code Pending +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt new file mode 100644 index 0000000..04309e1 --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_steps_rolled_back.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Applied +20200831110752000019 identity_verifiable_address_remove_code Applied +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt new file mode 100644 index 0000000..6961f8d --- /dev/null +++ b/oryx/popx/.snapshots/TestMigrateSQLUp-status_two_versions_rolled_back.txt @@ -0,0 +1,212 @@ +stdout: Version Name Status +20191100000001000000 identities Applied +20191100000001000001 identities Applied +20191100000001000002 identities Applied +20191100000001000003 identities Applied +20191100000001000004 identities Applied +20191100000001000005 identities Applied +20191100000002000000 requests Applied +20191100000002000001 requests Applied +20191100000002000002 requests Applied +20191100000002000003 requests Applied +20191100000002000004 requests Applied +20191100000003000000 sessions Applied +20191100000004000000 errors Applied +20191100000006000000 courier Applied +20191100000007000000 errors Applied +20191100000007000001 errors Applied +20191100000007000002 errors Applied +20191100000007000003 errors Applied +20191100000008000000 selfservice_verification Applied +20191100000008000001 selfservice_verification Applied +20191100000008000002 selfservice_verification Applied +20191100000008000003 selfservice_verification Applied +20191100000008000004 selfservice_verification Applied +20191100000008000005 selfservice_verification Applied +20191100000010000000 errors Applied +20191100000010000001 errors Applied +20191100000010000002 errors Applied +20191100000010000003 errors Applied +20191100000010000004 errors Applied +20191100000011000000 courier_body_type Applied +20191100000011000001 courier_body_type Applied +20191100000011000002 courier_body_type Applied +20191100000011000003 courier_body_type Applied +20191100000012000000 login_request_forced Applied +20191100000012000001 login_request_forced Applied +20191100000012000002 login_request_forced Applied +20191100000012000003 login_request_forced Applied +20200317160354000000 create_profile_request_forms Applied +20200317160354000001 create_profile_request_forms Applied +20200317160354000002 create_profile_request_forms Applied +20200317160354000003 create_profile_request_forms Applied +20200317160354000004 create_profile_request_forms Applied +20200317160354000005 create_profile_request_forms Applied +20200317160354000006 create_profile_request_forms Applied +20200401183443000000 continuity_containers Applied +20200402142539000000 rename_profile_flows Applied +20200402142539000001 rename_profile_flows Applied +20200402142539000002 rename_profile_flows Applied +20200519101057000000 create_recovery_addresses Applied +20200519101057000001 create_recovery_addresses Applied +20200519101057000002 create_recovery_addresses Applied +20200519101057000003 create_recovery_addresses Applied +20200519101057000004 create_recovery_addresses Applied +20200519101057000005 create_recovery_addresses Applied +20200519101057000006 create_recovery_addresses Applied +20200519101057000007 create_recovery_addresses Applied +20200601101000000000 create_messages Applied +20200601101000000001 create_messages Applied +20200601101000000002 create_messages Applied +20200601101000000003 create_messages Applied +20200605111551000000 messages Applied +20200605111551000001 messages Applied +20200605111551000002 messages Applied +20200605111551000003 messages Applied +20200605111551000004 messages Applied +20200605111551000005 messages Applied +20200605111551000006 messages Applied +20200605111551000007 messages Applied +20200605111551000008 messages Applied +20200605111551000009 messages Applied +20200605111551000010 messages Applied +20200605111551000011 messages Applied +20200607165100000000 settings Applied +20200607165100000001 settings Applied +20200607165100000002 settings Applied +20200607165100000003 settings Applied +20200607165100000004 settings Applied +20200705105359000000 rename_identities_schema Applied +20200810141652000000 flow_type Applied +20200810141652000001 flow_type Applied +20200810141652000002 flow_type Applied +20200810141652000003 flow_type Applied +20200810141652000004 flow_type Applied +20200810141652000005 flow_type Applied +20200810141652000006 flow_type Applied +20200810141652000007 flow_type Applied +20200810141652000008 flow_type Applied +20200810141652000009 flow_type Applied +20200810141652000010 flow_type Applied +20200810141652000011 flow_type Applied +20200810141652000012 flow_type Applied +20200810141652000013 flow_type Applied +20200810141652000014 flow_type Applied +20200810141652000015 flow_type Applied +20200810141652000016 flow_type Applied +20200810141652000017 flow_type Applied +20200810141652000018 flow_type Applied +20200810141652000019 flow_type Applied +20200810161022000000 flow_rename Applied +20200810161022000001 flow_rename Applied +20200810161022000002 flow_rename Applied +20200810161022000003 flow_rename Applied +20200810161022000004 flow_rename Applied +20200810161022000005 flow_rename Applied +20200810161022000006 flow_rename Applied +20200810161022000007 flow_rename Applied +20200810161022000008 flow_rename Applied +20200810162450000000 flow_fields_rename Applied +20200810162450000001 flow_fields_rename Applied +20200810162450000002 flow_fields_rename Applied +20200810162450000003 flow_fields_rename Applied +20200812124254000000 add_session_token Applied +20200812124254000001 add_session_token Applied +20200812124254000002 add_session_token Applied +20200812124254000003 add_session_token Applied +20200812124254000004 add_session_token Applied +20200812124254000005 add_session_token Applied +20200812124254000006 add_session_token Applied +20200812124254000007 add_session_token Applied +20200812160551000000 add_session_revoke Applied +20200812160551000001 add_session_revoke Applied +20200812160551000002 add_session_revoke Applied +20200812160551000003 add_session_revoke Applied +20200812160551000004 add_session_revoke Applied +20200812160551000005 add_session_revoke Applied +20200812160551000006 add_session_revoke Applied +20200812160551000007 add_session_revoke Applied +20200830121710000000 update_recovery_token Applied +20200830130642000000 add_verification_methods Applied +20200830130642000001 add_verification_methods Applied +20200830130642000002 add_verification_methods Applied +20200830130642000003 add_verification_methods Applied +20200830130642000004 add_verification_methods Applied +20200830130642000005 add_verification_methods Applied +20200830130642000006 add_verification_methods Applied +20200830130642000007 add_verification_methods Applied +20200830130642000008 add_verification_methods Applied +20200830130642000009 add_verification_methods Applied +20200830130642000010 add_verification_methods Applied +20200830130643000000 add_verification_methods Applied +20200830130644000000 add_verification_methods Applied +20200830130644000001 add_verification_methods Applied +20200830130645000000 add_verification_methods Applied +20200830130646000000 add_verification_methods Applied +20200830130646000001 add_verification_methods Applied +20200830130646000002 add_verification_methods Applied +20200830130646000003 add_verification_methods Applied +20200830130646000004 add_verification_methods Applied +20200830130646000005 add_verification_methods Applied +20200830130646000006 add_verification_methods Applied +20200830130646000007 add_verification_methods Applied +20200830130646000008 add_verification_methods Applied +20200830130646000009 add_verification_methods Applied +20200830130646000010 add_verification_methods Applied +20200830130646000011 add_verification_methods Applied +20200830154602000000 add_verification_token Applied +20200830154602000001 add_verification_token Applied +20200830154602000002 add_verification_token Applied +20200830154602000003 add_verification_token Applied +20200830154602000004 add_verification_token Applied +20200830172221000000 recovery_token_expires Applied +20200830172221000001 recovery_token_expires Applied +20200830172221000002 recovery_token_expires Applied +20200830172221000003 recovery_token_expires Applied +20200830172221000004 recovery_token_expires Applied +20200830172221000005 recovery_token_expires Applied +20200830172221000006 recovery_token_expires Applied +20200830172221000007 recovery_token_expires Applied +20200830172221000008 recovery_token_expires Applied +20200830172221000009 recovery_token_expires Applied +20200830172221000010 recovery_token_expires Applied +20200830172221000011 recovery_token_expires Applied +20200830172221000012 recovery_token_expires Applied +20200830172221000013 recovery_token_expires Applied +20200830172221000014 recovery_token_expires Applied +20200830172221000015 recovery_token_expires Applied +20200830172221000016 recovery_token_expires Applied +20200830172221000017 recovery_token_expires Applied +20200830172221000018 recovery_token_expires Applied +20200830172221000019 recovery_token_expires Applied +20200830172221000020 recovery_token_expires Applied +20200830172221000021 recovery_token_expires Applied +20200830172221000022 recovery_token_expires Applied +20200830172221000023 recovery_token_expires Applied +20200830172221000024 recovery_token_expires Applied +20200831110752000000 identity_verifiable_address_remove_code Applied +20200831110752000001 identity_verifiable_address_remove_code Applied +20200831110752000002 identity_verifiable_address_remove_code Applied +20200831110752000003 identity_verifiable_address_remove_code Applied +20200831110752000004 identity_verifiable_address_remove_code Applied +20200831110752000005 identity_verifiable_address_remove_code Applied +20200831110752000006 identity_verifiable_address_remove_code Applied +20200831110752000007 identity_verifiable_address_remove_code Applied +20200831110752000008 identity_verifiable_address_remove_code Applied +20200831110752000009 identity_verifiable_address_remove_code Applied +20200831110752000010 identity_verifiable_address_remove_code Applied +20200831110752000011 identity_verifiable_address_remove_code Applied +20200831110752000012 identity_verifiable_address_remove_code Applied +20200831110752000013 identity_verifiable_address_remove_code Applied +20200831110752000014 identity_verifiable_address_remove_code Applied +20200831110752000015 identity_verifiable_address_remove_code Applied +20200831110752000016 identity_verifiable_address_remove_code Applied +20200831110752000017 identity_verifiable_address_remove_code Applied +20200831110752000018 identity_verifiable_address_remove_code Pending +20200831110752000019 identity_verifiable_address_remove_code Pending +20200831110752000020 identity_verifiable_address_remove_code Pending +20200831110752000021 identity_verifiable_address_remove_code Pending +20201201161451000000 credential_types_values Pending +20201201161451000001 credential_types_values Pending + +stderr: diff --git a/oryx/popx/cmd.go b/oryx/popx/cmd.go new file mode 100644 index 0000000..321e876 --- /dev/null +++ b/oryx/popx/cmd.go @@ -0,0 +1,270 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/ory/x/cmdx" + "github.com/ory/x/errorsx" + "github.com/ory/x/flagx" +) + +type MigrationProvider interface { + MigrationStatus(context.Context) (MigrationStatuses, error) + MigrateUp(context.Context) error + MigrateDown(context.Context, int) error +} + +type MigrationPreparer interface { + PrepareMigration(context.Context) error +} + +func RegisterMigrateSQLUpFlags(cmd *cobra.Command) *cobra.Command { + cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + return cmd +} + +func NewMigrateSQLUpCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateSQLUpFlags(&cobra.Command{ + Use: "up [database_url]", + Args: cobra.RangeArgs(0, 1), + Short: "Apply all pending SQL migrations", + Long: `This command applies all pending SQL migrations for Ory {{ title .Root.Name }}. + +:::warning + +Before running this command, create a backup of your database. This command can be destructive as it may apply changes that cannot be easily reverted. Run this command close to the SQL instance (same VPC / same machine). + +::: + +It is recommended to review the migrations before running them. You can do this by running the command without the --yes flag: + + DSN=... {{ .CommandPath }} -e`, + Example: `Apply all pending migrations: + DSN=... {{ .CommandPath }} -e + +Apply all pending migrations: + DSN=... {{ .CommandPath }} -e --yes`, + RunE: runE, + }) +} + +func MigrateSQLUp(cmd *cobra.Command, p MigrationProvider) (err error) { + // convert migration tables + if prep, ok := p.(MigrationPreparer); ok { + if err := prep.PrepareMigration(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + // print migration status + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The migration plan is as follows:") + + // print migration status + status, err := p.MigrationStatus(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + cmdx.PrintTable(cmd, status) + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe SQL statements to be executed from top to bottom are:\n\n") + for i := range status { + if status[i].State == Pending { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ %s - %s ------------\n", status[i].Version, status[i].Name) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", status[i].ContentUp) + } + } + + if !flagx.MustGetBool(cmd, "yes") { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).") + if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ WARNING ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") + return nil + } + } + + // apply migrations + if err := p.MigrateUp(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ ERROR ------------\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ SUCCESS ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!") + return nil +} + +func RegisterMigrateSQLDownFlags(cmd *cobra.Command) *cobra.Command { + cmd.Flags().BoolP("yes", "y", false, "If set all confirmation requests are accepted without user interaction.") + cmd.Flags().Int("steps", 0, "The number of migrations to roll back.") + return cmd +} + +func NewMigrateSQLDownCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateSQLDownFlags(&cobra.Command{ + Use: "down [database_url]", + Args: cobra.RangeArgs(0, 1), + Short: "Rollback the last applied SQL migrations", + Long: `This command rolls back the last applied SQL migrations for Ory {{ title .Root.Name }}. + +:::warning + +Before running this command, create a backup of your database. This command can be destructive as it may revert changes made by previous migrations. Run this command close to the SQL instance (same VPC / same machine). + +::: + +It is recommended to review the migrations before running them. You can do this by running the command without the --yes flag: + + DSN=... {{ .CommandPath }} -e`, + Example: `See the current migration status: + DSN=... {{ .CommandPath }} -e + +Rollback the last 10 migrations: + {{ .CommandPath }} $DSN --steps 10 + +Rollback the last 10 migrations without confirmation: + DSN=... {{ .CommandPath }} -e --yes --steps 10`, + RunE: runE, + }) +} + +func MigrateSQLDown(cmd *cobra.Command, p MigrationProvider) (err error) { + steps := flagx.MustGetInt(cmd, "steps") + if steps < 0 { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Flag --steps must be larger than 0.") + return cmdx.FailSilently(cmd) + } + + // convert migration tables + if prep, ok := p.(MigrationPreparer); ok { + if err := prep.PrepareMigration(cmd.Context()); err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not convert the migration table:\n%+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + status, err := p.MigrationStatus(cmd.Context()) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get the migration status:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + // Now we need to rollback the last `steps` migrations that have a status of "Applied": + var count int + var rollingBack int + for i := len(status) - 1; i >= 0; i-- { + if status[i].State == Applied { + count++ + if steps > 0 && count <= steps { + status[i].State = "Rollback" + rollingBack++ + } + } + } + + // print migration status + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "The migration plan is as follows:") + cmdx.PrintTable(cmd, status) + + if rollingBack < 1 { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "There are apparently no migrations to roll back.") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Please provide the --steps argument with a value larger than 0.") + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "") + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "\nThe SQL statements to be executed from top to bottom are:\n\n") + + for i := len(status) - 1; i >= 0; i-- { + if status[i].State == "Rollback" { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ %s - %s ------------\n", status[i].Version, status[i].Name) + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s\n\n", status[i].ContentDown) + } + } + + if !flagx.MustGetBool(cmd, "yes") { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), "To skip the next question use flag --yes (at your own risk).") + if !cmdx.AskForConfirmation("Do you wish to execute this migration plan?", cmd.InOrStdin(), cmd.OutOrStdout()) { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ WARNING ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Migration aborted.") + return nil + } + } + + // apply migrations + if err := p.MigrateDown(cmd.Context(), rollingBack); err != nil { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ ERROR ------------\n") + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not apply migrations:\n%+v\n", errorsx.WithStack(err)) + return cmdx.FailSilently(cmd) + } + + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "------------ SUCCESS ------------\n") + _, _ = fmt.Fprintln(cmd.OutOrStdout(), "Successfully applied migrations!") + return nil +} + +func RegisterMigrateStatusFlags(cmd *cobra.Command) *cobra.Command { + cmdx.RegisterFormatFlags(cmd.PersistentFlags()) + cmd.Flags().BoolP("read-from-env", "e", false, "If set, reads the database connection string from the environment variable DSN or config file key dsn.") + cmd.Flags().Bool("block", false, "Block until all migrations have been applied") + return cmd +} + +func NewMigrateSQLStatusCmd(runE func(cmd *cobra.Command, args []string) error) *cobra.Command { + return RegisterMigrateStatusFlags(&cobra.Command{ + Use: "status [database_url]", + Short: "Display the current migration status", + Long: `This command shows the current migration status for Ory {{ title .Root.Name }}. + +You can use this command to check which migrations have been applied and which are pending. + +To block until all migrations are applied, use the --block flag: + + DSN=... {{ .CommandPath }} -e --block`, + Example: `See the current migration status: + DSN=... {{ .CommandPath }} -e + +Block until all migrations are applied: + DSN=... {{ .CommandPath }} -e --block`, + RunE: runE, + }) +} + +func MigrateStatus(cmd *cobra.Command, p MigrationProvider) (err error) { + block := flagx.MustGetBool(cmd, "block") + ctx := cmd.Context() + s, err := p.MigrationStatus(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + + for block && s.HasPending() { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), "Waiting for migrations to finish...\n") + for _, m := range s { + if m.State == Pending { + _, _ = fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", m.Name) + } + } + time.Sleep(time.Second) + s, err = p.MigrationStatus(ctx) + if err != nil { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not get migration status: %+v\n", err) + return cmdx.FailSilently(cmd) + } + } + + cmdx.PrintTable(cmd, s) + return nil +} diff --git a/oryx/popx/db_columns.go b/oryx/popx/db_columns.go new file mode 100644 index 0000000..99132c1 --- /dev/null +++ b/oryx/popx/db_columns.go @@ -0,0 +1,60 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "fmt" + "strings" + + "github.com/ory/pop/v6" +) + +func DBColumns[T any](quoter Quoter) string { + return (&pop.Model{Value: new(T)}).Columns().QuotedString(quoter) +} + +// IndexHint returns the table name including the index hint, if the database +// supports it. +func IndexHint(conn *pop.Connection, table string, index string) string { + if conn.Dialect.Name() == "cockroach" { + return table + "@" + index + } + // PostgreSQL and YugabyteDB do not accept CockroachDB's @index syntax. + return table +} + +func DBColumnsExcluding[T any](quoter Quoter, exclude ...string) string { + cols := (&pop.Model{Value: new(T)}).Columns() + for _, e := range exclude { + cols.Remove(e) + } + return cols.QuotedString(quoter) +} + +type ( + AliasQuoter struct { + Alias string + Quoter Quoter + } + Quoter interface { + Quote(key string) string + } +) + +func (pq *AliasQuoter) Quote(key string) string { + return fmt.Sprintf("%s.%s", pq.Quoter.Quote(pq.Alias), pq.Quoter.Quote(key)) +} + +// Placeholders returns `num` placeholders ?, e.g. "?, ?, ?". +// Placeholders(0) returns "". +func Placeholders(num int) string { + switch num { + case 0: + return "" + case 1: + return "?" + default: + return strings.Repeat("?, ", num-1) + "?" + } +} diff --git a/oryx/popx/dialect_check.go b/oryx/popx/dialect_check.go new file mode 100644 index 0000000..76ba47d --- /dev/null +++ b/oryx/popx/dialect_check.go @@ -0,0 +1,78 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" +) + +// VerifyDialect makes sure the declared SQL dialect matches the actual +// database server. It distinguishes PostgreSQL, CockroachDB, and YugabyteDB: +// all three speak the Postgres wire protocol, so an operator can easily point +// a service configured for one at a server running another. That mismatch +// silently selects the wrong dialect-specific migrations and produces broken +// schemas. +// +// Other dialects (MySQL, SQLite) are skipped. +func VerifyDialect(ctx context.Context, conn *pop.Connection) error { + declared := conn.Dialect.Name() + if declared != namePostgres && declared != dbal.DriverCockroachDB && declared != dbal.DriverYugabyteDB { + return nil + } + + var version string + if err := conn.WithContext(ctx).RawQuery("SELECT version() AS version").First(&version); err != nil { + return errors.Wrap(err, "could not query database version to verify dialect") + } + return checkDialect(declared, version, pop.DialectSupported(dbal.DriverYugabyteDB)) +} + +const ( + namePostgres = "postgres" + + // yugabyteVersionMarker identifies a YugabyteDB server in the output of + // SELECT version() (e.g. "PostgreSQL 11.2-YB-2025.2.4.0-b122 ..."). + yugabyteVersionMarker = "-YB-" +) + +func checkDialect(declared, version string, yugabyteDialectSupported bool) error { + detectedCockroach := strings.Contains(version, "CockroachDB") + detectedYugabyte := strings.Contains(version, yugabyteVersionMarker) + switch { + case declared == dbal.DriverCockroachDB && !detectedCockroach: + return errors.Errorf( + "DSN scheme cockroach:// declares a CockroachDB database but the server is not CockroachDB. Server reported: %q. Use a postgres:// DSN, or point the service at a CockroachDB cluster.", + firstLine(version), + ) + case declared == dbal.DriverYugabyteDB && !detectedYugabyte: + return errors.Errorf( + "DSN scheme yugabyte:// declares a YugabyteDB database but the server is not YugabyteDB. Server reported: %q. Use a DSN scheme matching the actual server, or point the service at a YugabyteDB cluster.", + firstLine(version), + ) + case declared == namePostgres && detectedCockroach: + return errors.Errorf( + "DSN scheme postgres:// declares a PostgreSQL database but the server is CockroachDB. Replace the scheme with cockroach:// so that the service picks the correct migrations and SQL dialect. Server reported: %q", + firstLine(version), + ) + case declared == namePostgres && detectedYugabyte && yugabyteDialectSupported: + return errors.Errorf( + "DSN scheme postgres:// declares a PostgreSQL database but the server is YugabyteDB. Replace the scheme with yugabyte:// (supported in Ory Enterprise License builds) so that the service picks the correct migrations. Server reported: %q", + firstLine(version), + ) + } + return nil +} + +func firstLine(s string) string { + if i := strings.IndexByte(s, '\n'); i >= 0 { + return s[:i] + } + return s +} diff --git a/oryx/popx/loggers.go b/oryx/popx/loggers.go new file mode 100644 index 0000000..d992190 --- /dev/null +++ b/oryx/popx/loggers.go @@ -0,0 +1,53 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "fmt" + "testing" + + "github.com/ory/pop/v6" + "github.com/ory/pop/v6/logging" +) + +func formatter(lvl logging.Level, s string, args ...interface{}) string { + if pop.Debug == false { + return "" + } + + if lvl == logging.SQL { + if len(args) > 0 { + xargs := make([]string, len(args)) + for i, a := range args { + switch a.(type) { + case string: + xargs[i] = fmt.Sprintf("%q", a) + default: + xargs[i] = fmt.Sprintf("%v", a) + } + } + s = fmt.Sprintf("%s - %s | %s", lvl, s, xargs) + } else { + s = fmt.Sprintf("%s - %s", lvl, s) + } + } else { + s = fmt.Sprintf(s, args...) + s = fmt.Sprintf("%s - %s", lvl, s) + } + return s +} + +func TestingLogger(t testing.TB) func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + if line := formatter(lvl, s, args...); len(line) > 0 { + t.Log(line) + } + } +} + +func NullLogger() func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + // do nothing + } +} diff --git a/oryx/popx/match.go b/oryx/popx/match.go new file mode 100644 index 0000000..5d1631c --- /dev/null +++ b/oryx/popx/match.go @@ -0,0 +1,97 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "regexp" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" +) + +var errUnsupportedMigrationDialect = errors.New("unsupported dialect") + +var MigrationFileRegexp = regexp.MustCompile( + `^(\d+)_([^.]+)(\.[a-z0-9]+)?(\.autocommit)?\.(up|down)\.(sql)$`, +) + +const ( + // Human-readable constants for the regex capture groups + versionIdx = iota + 1 + nameIdx + dbTypeIdx + autocommitIdx + directionIdx + typeIdx +) + +// match holds the information parsed from a migration filename. +type match struct { + Version string + Name string + DBType string + Direction string + Type string + Autocommit bool +} + +// parseMigrationFilename parses a migration filename. +func parseMigrationFilename(filename string) (*match, error) { + matches := MigrationFileRegexp.FindAllStringSubmatch(filename, -1) + if len(matches) == 0 { + return nil, nil + } + m := matches[0] + + var ( + autocommit bool + dbType string + ) + + if m[dbTypeIdx] == ".autocommit" { + // A special case where autocommit group moves forward to the 3rd index. + autocommit = true + dbType = "all" + } else if m[dbTypeIdx] == "" { + dbType = "all" + } else { + dbType = strings.TrimPrefix(m[dbTypeIdx], ".") + // The commercial package registers this dialect from init(), but + // migration boxes can be constructed before that package enters the + // import graph. Canonicalize both accepted spellings independently of + // registration so migration selection is deterministic in every build. + switch dbType { + case dbal.DriverYugabyteDB, "yugabytedb": + dbType = dbal.DriverYugabyteDB + default: + dbType = pop.CanonicalDialect(dbType) + } + if dbType != dbal.DriverYugabyteDB && !pop.DialectSupported(dbType) { + return nil, errors.Wrapf(errUnsupportedMigrationDialect, "%s", dbType) + } + } + + if m[typeIdx] == "fizz" && dbType != "all" { + return nil, errors.Errorf("invalid database type %q, expected \"all\" because fizz is database type independent", dbType) + } + + if m[autocommitIdx] == ".autocommit" { + autocommit = true + } else if m[autocommitIdx] != "" { + return nil, errors.Errorf("invalid autocommit flag %q", m[autocommitIdx]) + } + + return &match{ + Version: m[versionIdx], + Name: m[nameIdx], + DBType: dbType, + Autocommit: autocommit, + Direction: m[directionIdx], + Type: m[typeIdx], + }, nil +} diff --git a/oryx/popx/migration_box.go b/oryx/popx/migration_box.go new file mode 100644 index 0000000..6ec4a40 --- /dev/null +++ b/oryx/popx/migration_box.go @@ -0,0 +1,371 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "database/sql" + "fmt" + "io/fs" + "path" + "regexp" + "slices" + "sort" + "strings" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + + "github.com/ory/x/dbal" + "github.com/ory/x/logrusx" +) + +type ( + // MigrationBox is a embed migration box. + MigrationBox struct { + c *pop.Connection + migrationsUp Migrations + migrationsDown Migrations + perMigrationTimeout time.Duration + l *logrusx.Logger + migrationContent MigrationContent + disableGoldenDatabase bool + sqliteTemplateCacheDir string + } + MigrationContent func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) + MigrationBoxOption func(*MigrationBox) +) + +func WithTemplateValues(v map[string]interface{}) MigrationBoxOption { + return func(m *MigrationBox) { + m.migrationContent = ParameterizedMigrationContent(v) + } +} + +func WithMigrationContentMiddleware(middleware func(content string, err error) (string, error)) MigrationBoxOption { + return func(m *MigrationBox) { + prev := m.migrationContent + m.migrationContent = func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) { + return middleware(prev(mf, c, r, usingTemplate)) + } + } +} + +// WithGoMigrations adds migrations that have a custom migration runner. +// TEST THEM THOROUGHLY! +// It will be very hard to fix a buggy migration. +func WithGoMigrations(migrations Migrations) MigrationBoxOption { + return func(mb *MigrationBox) { + for _, m := range migrations { + switch m.Direction { + case "up": + mb.migrationsUp = append(mb.migrationsUp, m) + case "down": + mb.migrationsDown = append(mb.migrationsDown, m) + default: + panic(fmt.Sprintf("unknown migration direction %q for %q", m.Direction, m.Version)) + } + } + } +} + +func WithPerMigrationTimeout(timeout time.Duration) MigrationBoxOption { + return func(m *MigrationBox) { + m.perMigrationTimeout = timeout + } +} + +// WithoutGoldenDatabase disables the golden database optimisation for SQLite +// test databases. Use this when the caller asserts on migration execution +// side-effects (e.g. exact timing, call counts, or intermediate schema state). +func WithoutGoldenDatabase() MigrationBoxOption { + return func(m *MigrationBox) { + m.disableGoldenDatabase = true + } +} + +// WithSQLiteTemplateCacheDir stores pre-migrated SQLite test database +// templates in dir. By default, templates are stored in os.TempDir. +// +// This option only affects on-disk SQLite databases while running tests. +func WithSQLiteTemplateCacheDir(dir string) MigrationBoxOption { + return func(m *MigrationBox) { + m.sqliteTemplateCacheDir = dir + } +} + +var testdataPattern = regexp.MustCompile(`^(\d+)_testdata(|\.[a-zA-Z0-9]+).sql$`) + +// WithTestdata adds testdata to the migration box. +func WithTestdata(t *testing.T, testdata fs.FS) MigrationBoxOption { + return func(m *MigrationBox) { + // Testdata may not change the golden-database hash, + // so it should not share or create a golden database. + m.disableGoldenDatabase = true + require.NoError(t, fs.WalkDir(testdata, ".", func(path string, info fs.DirEntry, err error) error { + if err != nil { + return errors.WithStack(err) + } + if info.Name() == "." { + return nil + } + if !info.Type().IsRegular() { + t.Logf("skipping testdata entry that is not a file: %s", path) + return nil + } + + match := testdataPattern.FindStringSubmatch(info.Name()) + if len(match) != 2 && len(match) != 3 { + t.Logf(`WARNING! Found a test migration which does not match the test data pattern: %s`, info.Name()) + return nil + } + + version := match[1] + flavor := "all" + if len(match) == 3 && len(match[2]) > 0 { + flavor = pop.CanonicalDialect(strings.TrimPrefix(match[2], ".")) + } + + // t.Logf("Found test migration \"%s\" (%s, %+v): %s", flavor, match, err, info.Name()) + + m.migrationsUp = append(m.migrationsUp, Migration{ + Version: version + "9", // run testdata after version + Path: path, + Name: info.Name(), + DBType: flavor, + Direction: "up", + Type: "sql", + Runner: func(m Migration, c *pop.Connection) error { + b, err := fs.ReadFile(testdata, m.Path) + if err != nil { + return errors.WithStack(err) + } + if isMigrationEmpty(string(b)) { + return nil + } + _, err = c.Store.SQLDB().Exec(string(b)) + return errors.WithStack(err) + }, + }) + + m.migrationsDown = append(m.migrationsDown, Migration{ + Version: version + "9", // run testdata after version + Path: path, + Name: info.Name(), + DBType: flavor, + Direction: "down", + Type: "sql", + Runner: func(m Migration, _ *pop.Connection) error { return nil }, + }) + + return nil + })) + } +} + +var emptySQLReplace = regexp.MustCompile(`(?m)^(\s*--.*|\s*)$`) + +func isMigrationEmpty(content string) bool { + return len(strings.ReplaceAll(emptySQLReplace.ReplaceAllString(content, ""), "\n", "")) == 0 +} + +type queryExecutor interface { + Exec(query string, args ...any) (sql.Result, error) +} + +// NewMigrationBox creates a new migration box. +func NewMigrationBox(dir fs.FS, c *pop.Connection, l *logrusx.Logger, opts ...MigrationBoxOption) (*MigrationBox, error) { + mb := &MigrationBox{ + c: c, + l: l, + migrationContent: ParameterizedMigrationContent(nil), + } + + for _, o := range opts { + o(mb) + } + + txRunner := func(b []byte) func(Migration, *pop.Connection) error { + return func(mf Migration, c *pop.Connection) error { + content, err := mb.migrationContent(mf, c, b, true) + if err != nil { + return errors.Wrapf(err, "error processing %s", mf.Path) + } + if isMigrationEmpty(content) { + l.WithField("migration", mf.Path).Trace("This is usually ok - ignoring migration because content is empty. This is ok!") + return nil + } + + var q queryExecutor = c.Store.SQLDB() + if c.TX != nil { + q = c.TX + } + + if _, err = q.Exec(content); err != nil { + return errors.Wrapf(err, "error executing %s, sql: %s", mf.Path, content) + } + return nil + } + } + + err := mb.findMigrations(dir, txRunner) + if err != nil { + return mb, err + } + + if err := mb.check(); err != nil { + return nil, err + } + return mb, nil +} + +// migrationFallbacks returns the ordered dialects migration selection falls +// back to before the generic ("all") file (.yugabyte. → .postgres. → generic +// for YugabyteDB, no fallbacks otherwise). +func (mb *MigrationBox) migrationFallbacks() []string { + if mb.c.Dialect.Name() == dbal.DriverYugabyteDB { + return []string{dbal.DriverPostgreSQL} + } + return nil +} + +// SelectedMigrations returns a copy of the migrations selected for the +// connection's dialect and direction. It uses the same exact-match and fallback +// ranking as Up, Down, and Status so callers can audit the effective migration +// set without duplicating filename-selection logic. +func (mb *MigrationBox) SelectedMigrations(direction string) (Migrations, error) { + dialect := mb.c.Dialect.Name() + fallbacks := mb.migrationFallbacks() + switch direction { + case "up": + return slices.Clone(mb.migrationsUp.sortAndFilter(dialect, fallbacks...)), nil + case "down": + return slices.Clone(mb.migrationsDown.sortAndFilter(dialect, fallbacks...)), nil + default: + return nil, errors.Errorf("unknown migration direction %q", direction) + } +} + +// noTxDDL reports whether the dialect cannot run DDL inside a transaction, so +// both migrations and the migration-status table setup must run in autocommit +// mode. CockroachDB and MySQL auto-commit each DDL statement; YugabyteDB +// restricts DDL inside transactions. Because there is no surrounding +// transaction to roll back a partial failure, new YugabyteDB migrations must +// contain one idempotent statement per file. YugabyteDB also inherits older +// PostgreSQL migration files that predate this rule; service-level guard tests +// track that replay risk, and a dedicated .yugabyte. override is required when +// a new unsafe inherited migration appears. +func (mb *MigrationBox) noTxDDL() bool { + switch mb.c.Dialect.Name() { + case dbal.DriverCockroachDB, dbal.DriverMySQL, dbal.DriverYugabyteDB: + return true + } + return false +} + +func (mb *MigrationBox) findMigrations( + dir fs.FS, + runner func([]byte) func(m Migration, c *pop.Connection) error, +) error { + err := fs.WalkDir(dir, ".", func(p string, info fs.DirEntry, err error) error { + if err != nil { + return errors.WithStack(err) + } + + if !info.Type().IsRegular() { + mb.l.Tracef("ignoring non file: %s", info.Name()) + return nil + } + + if path.Ext(info.Name()) != ".sql" { + mb.l.Tracef("ignoring non SQL file: %s", info.Name()) + return nil + } + + details, err := parseMigrationFilename(info.Name()) + if err != nil { + if errors.Is(err, errUnsupportedMigrationDialect) { + mb.l.Debugf("Ignoring migration file %s because its dialect is not supported: %s", info.Name(), err.Error()) + return nil + } + return errors.WithStack(err) + } + + if details == nil { + return errors.Errorf("Found a migration file that does not match the file pattern: filename=%s pattern=%s", info.Name(), MigrationFileRegexp) + } + + content, err := fs.ReadFile(dir, p) + if err != nil { + return errors.WithStack(err) + } + + mf := Migration{ + Path: p, + Version: details.Version, + Name: details.Name, + DBType: details.DBType, + Direction: details.Direction, + Type: details.Type, + Content: string(content), + Autocommit: details.Autocommit, + } + + mf.Runner = runner(content) + + switch details.Direction { + case "up": + mb.migrationsUp = append(mb.migrationsUp, mf) + case "down": + mb.migrationsDown = append(mb.migrationsDown, mf) + default: + return errors.Errorf("unknown migration direction %q for %q", details.Direction, info.Name()) + } + return nil + }) + + // Sort descending. + sort.Sort(mb.migrationsDown) + slices.Reverse(mb.migrationsDown) + + // Sort ascending. + sort.Sort(mb.migrationsUp) + + return errors.WithStack(err) +} + +// hasDownMigrationWithVersion checks if there is a migration with the given +// version. +func (mb *MigrationBox) hasDownMigrationWithVersion(version string) bool { + for _, down := range mb.migrationsDown { + if version == down.Version { + return true + } + } + return false +} + +// check checks that every "up" migration has a corresponding "down" migration. +func (mb *MigrationBox) check() error { + for _, up := range mb.migrationsUp { + if !mb.hasDownMigrationWithVersion(up.Version) { + return errors.Errorf("migration %s has no corresponding down migration", up.Version) + } + } + + for _, n := range mb.migrationsUp { + if err := n.Valid(); err != nil { + return errors.WithStack(err) + } + } + for _, n := range mb.migrationsDown { + if err := n.Valid(); err != nil { + return errors.WithStack(err) + } + } + return nil +} diff --git a/oryx/popx/migration_content.go b/oryx/popx/migration_content.go new file mode 100644 index 0000000..a310b28 --- /dev/null +++ b/oryx/popx/migration_content.go @@ -0,0 +1,54 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "bytes" + "text/template" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/dbal" +) + +func ParameterizedMigrationContent(params map[string]interface{}) func(mf Migration, c *pop.Connection, r []byte, usingTemplate bool) (string, error) { + return func(mf Migration, c *pop.Connection, b []byte, usingTemplate bool) (string, error) { + content := "" + if usingTemplate { + t := template.New("migration") + t.Funcs(SQLTemplateFuncs) + t, err := t.Parse(string(b)) + if err != nil { + return "", errors.Wrapf(err, "could not parse template %s", mf.Path) + } + var bb bytes.Buffer + err = t.Execute(&bb, struct { + IsSQLite bool + IsCockroach bool + IsMySQL bool + IsMariaDB bool + IsPostgreSQL bool + DialectDetails *pop.ConnectionDetails + Parameters map[string]interface{} + }{ + IsSQLite: c.Dialect.Name() == "sqlite3", + IsCockroach: c.Dialect.Name() == "cockroach", + IsMySQL: c.Dialect.Name() == "mysql", + IsMariaDB: c.Dialect.Name() == "mariadb", + IsPostgreSQL: dbal.IsPostgresCompatible(c.Dialect.Name()), + DialectDetails: c.Dialect.Details(), + Parameters: params, + }) + if err != nil { + return "", errors.Wrapf(err, "could not execute migration template %s", mf.Path) + } + content = bb.String() + } else { + content = string(b) + } + + return content, nil + } +} diff --git a/oryx/popx/migration_info.go b/oryx/popx/migration_info.go new file mode 100644 index 0000000..9b93fa6 --- /dev/null +++ b/oryx/popx/migration_info.go @@ -0,0 +1,130 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "sort" + "strings" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" +) + +// Migration handles the data for a given database migration +type Migration struct { + // Path to the migration (./migrations/123_create_widgets.up.sql) + Path string + // Version of the migration (123) + Version string + // Name of the migration (create_widgets) + Name string + // Direction of the migration (up|down) + Direction string + // Type of migration (sql|go) + Type string + // DB type (all|postgres|mysql...) + DBType string + // Runner function to run/execute the migration. Will be wrapped in a + // database transaction. Mutually exclusive with RunnerNoTx + Runner func(Migration, *pop.Connection) error + // Content is the raw content of the migration file + Content string + // Autocommit indicates whether the migration should be run in autocommit mode + Autocommit bool +} + +func (m Migration) Valid() error { + if m.Runner == nil { + return errors.Errorf("no runner defined for %s", m.Path) + } + + return nil +} + +// Migrations is a collection of Migration +type Migrations []Migration + +func (mfs Migrations) Len() int { return len(mfs) } +func (mfs Migrations) Less(i, j int) bool { return compareMigration(mfs[i], mfs[j]) < 0 } +func (mfs Migrations) Swap(i, j int) { mfs[i], mfs[j] = mfs[j], mfs[i] } + +func compareMigration(a, b Migration) int { + if a.Version != b.Version { + return strings.Compare(a.Version, b.Version) + } + // Force "all" to be greater. + if a.DBType == "all" && b.DBType != "all" { + return 1 + } else if a.DBType != "all" && b.DBType == "all" { + return -1 + } + return strings.Compare(a.DBType, b.DBType) +} + +// migrationRank ranks a migration's DBType for the given dialect and its ordered +// fallbacks. Higher numbers indicate a better match: +// +// -1 not applicable +// 0 "all" (generic, dialect-independent) +// 1..n a fallback dialect (earlier fallbacks rank higher) +// n+1 the exact dialect +// +// This lets a migration box prefer a dialect-specific override, then a fallback +// dialect (e.g. postgres for a postgres-wire-compatible database), then the +// generic file. +func migrationRank(dbType, dialect string, fallbacks []string) int { + switch dbType { + case dialect: + return len(fallbacks) + 1 + case "all": + return 0 + } + for i, fb := range fallbacks { + if dbType == fb { + return len(fallbacks) - i + } + } + return -1 +} + +func (mfs Migrations) sortAndFilter(dialect string, fallbacks ...string) Migrations { + byVersion := make(map[string]Migration, len(mfs)) + for _, migration := range mfs { + rank := migrationRank(migration.DBType, dialect, fallbacks) + if rank < 0 { + // Not applicable to this dialect. + continue + } + if previousMigration, ok := byVersion[migration.Version]; ok { + if rank < migrationRank(previousMigration.DBType, dialect, fallbacks) { + // Previous migration is a better match, skip this one. + continue + } + } + byVersion[migration.Version] = migration + } + + filtered := make(Migrations, 0, len(byVersion)) + for k := range byVersion { + filtered = append(filtered, byVersion[k]) + } + sort.Sort(filtered) + return filtered +} + +func (mfs Migrations) find(version, dialect string, fallbacks ...string) *Migration { + var candidate *Migration + bestRank := -1 + for i := range mfs { + if mfs[i].Version != version { + continue + } + if rank := migrationRank(mfs[i].DBType, dialect, fallbacks); rank > bestRank { + bestRank = rank + candidate = &mfs[i] + } + } + return candidate +} diff --git a/oryx/popx/migrator.go b/oryx/popx/migrator.go new file mode 100644 index 0000000..efdb37f --- /dev/null +++ b/oryx/popx/migrator.go @@ -0,0 +1,724 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "crypto/sha256" + "database/sql" + "database/sql/driver" + "encoding/hex" + "fmt" + "math" + "os" + "path/filepath" + "regexp" + "slices" + "strings" + "testing" + "time" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/gofrs/flock" + "github.com/pkg/errors" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + moderncsqlite "modernc.org/sqlite" + + "github.com/ory/pop/v6" + "github.com/ory/x/cmdx" + "github.com/ory/x/dbal" + "github.com/ory/x/logrusx" + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" +) + +const ( + Pending = "Pending" + Applied = "Applied" + tracingComponent = "github.com/ory/x/popx" +) + +func (mb *MigrationBox) shouldNotUseTransaction(m Migration) bool { + return m.Autocommit || mb.noTxDDL() +} + +// Up runs pending "up" migrations and applies them to the database. +func (mb *MigrationBox) Up(ctx context.Context) error { + _, err := mb.UpTo(ctx, 0) + return errors.WithStack(err) +} + +// sqliteTemplatePath computes the path of the SQLite template for this +// migration set. The path is deterministic and content-addressed, so a changed +// migration set cannot reuse an older template. +func (mb *MigrationBox) sqliteTemplatePath() string { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + h := sha256.New() + for _, mi := range mfs { + for _, part := range []string{ + mi.Version, + mi.Path, + mi.Name, + mi.Direction, + mi.Type, + mi.DBType, + mi.Content, + } { + _, _ = h.Write([]byte(part)) + _, _ = h.Write([]byte{0}) + } + } + dir := mb.sqliteTemplateCacheDir + if dir == "" { + dir = os.TempDir() + } + return filepath.Join(dir, "ory-popx-sqlite-template-"+hex.EncodeToString(h.Sum(nil))+".sqlite") +} + +// restoreSQLiteOnline streams the contents of srcPath into the database +// represented by db using SQLite's online backup API. The destination +// connection stays open throughout, so the *sql.DB and any pop.Connection +// holding it remain valid. +// +// pop wraps the modernc.org/sqlite driver with otelsql for tracing; we +// unwrap via the otelsql Raw() accessor before reaching the driver-specific +// NewRestore method on the underlying *sqlite.conn. +func restoreSQLiteOnline(ctx context.Context, db *sql.DB, srcPath string) error { + conn, err := db.Conn(ctx) + if err != nil { + return errors.Wrap(err, "failed to acquire sql.Conn for restore") + } + defer func() { _ = conn.Close() }() + + return conn.Raw(func(driverConn any) error { + if w, ok := driverConn.(interface{ Raw() driver.Conn }); ok { + driverConn = w.Raw() + } + restorer, ok := driverConn.(interface { + NewRestore(srcUri string) (*moderncsqlite.Backup, error) + }) + if !ok { + return errors.Errorf("driver %T does not support online restore", driverConn) + } + // See: https://sqlite.org/backup.html . + backup, err := restorer.NewRestore(srcPath) + if err != nil { + return errors.Wrap(err, "NewRestore failed") + } + // Step(-1) copies all remaining pages in one call. + for { + more, stepErr := backup.Step(-1) + if stepErr != nil { + _ = backup.Finish() + return errors.Wrap(stepErr, "backup.Step failed") + } + if !more { + break + } + } + return errors.Wrap(backup.Finish(), "backup.Finish failed") + }) +} + +// UpTo runs up to step "up" migrations and applies them to the database. +// If step <= 0 all pending migrations are run. +func (mb *MigrationBox) UpTo(ctx context.Context, step int) (applied int, err error) { + ctx, span := startSpan(ctx, MigrationUpOpName, trace.WithAttributes(attribute.Int("step", step))) + defer otelx.End(span, &err) + + c := mb.c.WithContext(ctx) + + newDbFileName, isOnDiskSQLite := sqliteFilePath(mb.c.URL()) + isSQLite := mb.c.Dialect.Name() == "sqlite3" + isOnDiskSQLite = isSQLite && isOnDiskSQLite + + // For test SQLite databases, try to restore a pre-migrated template + // using SQLite's online backup API. The restore streams pages directly into + // the open connection, so mb.c stays valid throughout and any holders of + // it (including WithContext copies) keep working. + if testing.Testing() && isOnDiskSQLite && step <= 0 && !mb.disableGoldenDatabase { + templatePath := mb.sqliteTemplatePath() + // Only restore onto a fresh (uninitialized) database. If the + // migration table already exists, migrations were previously applied + // — either directly or via an earlier template restore. + mtn := sanitizedMigrationTableName(mb.c) + var existingTableCount int + alreadyMigrated := mb.c.Store.SQLDB().QueryRow( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", mtn, + ).Scan(&existingTableCount) == nil && existingTableCount > 0 + if !alreadyMigrated { + if mkdirErr := os.MkdirAll(filepath.Dir(templatePath), 0o750); mkdirErr != nil { + mb.l.Errorf("Failed to create SQLite template cache directory, applying migrations: path=%s err=%v", filepath.Dir(templatePath), mkdirErr) + } else { + // Test packages run as separate processes. Serialize the first + // template build so only one of them pays the migration cost; + // waiters restore the completed template after acquiring the + // lock. The template itself is still installed atomically. + lock := flock.New(templatePath + ".lock") + if lockErr := lock.Lock(); lockErr != nil { + mb.l.Errorf("Failed to lock SQLite template, applying migrations: path=%s err=%v", templatePath, lockErr) + } else { + defer func() { + if unlockErr := lock.Unlock(); unlockErr != nil { + mb.l.Errorf("Failed to unlock SQLite template: path=%s err=%v", templatePath, unlockErr) + } + }() + } + } + + if _, statErr := os.Stat(templatePath); statErr == nil { + if restoreErr := restoreSQLiteOnline(ctx, mb.c.Store.SQLDB(), templatePath); restoreErr != nil { + // Restore failed; log and fall through to the normal migration path. + mb.l.Errorf("Failed to restore SQLite template, applying migrations: src=%s dst=%s err=%v", templatePath, newDbFileName, restoreErr) + } else { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + mb.l.Infof("Skipped applying %d migrations using SQLite template: path=%s", len(mfs), templatePath) + return len(mfs), nil + } + } else if !errors.Is(statErr, os.ErrNotExist) { + mb.l.Errorf("Failed to inspect SQLite template, applying migrations: path=%s err=%v", templatePath, statErr) + } + } + } + + // Keep applied across whole-run retry attempts. Every migration counted here + // has already committed its schema_migration row, so it remains part of this + // UpTo call's step budget even when a later migration retries. + err = mb.exec(ctx, func() error { + mtn := sanitizedMigrationTableName(c) + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + for _, mi := range mfs { + l := mb.l.WithField("version", mi.Version).WithField("migration_name", mi.Name).WithField("migration_file", mi.Path) + + appliedMigrations := make([]string, 0, 2) + legacyVersion := mi.Version + if len(legacyVersion) > 14 { + legacyVersion = legacyVersion[:14] + } + err := c.RawQuery(fmt.Sprintf("SELECT version FROM %s WHERE version IN (?, ?)", mtn), mi.Version, legacyVersion).All(&appliedMigrations) + if err != nil { + return errors.Wrapf(err, "problem checking for migration version %s", mi.Version) + } + + if slices.Contains(appliedMigrations, mi.Version) { + l.Debug("Migration has already been applied, skipping.") + continue + } + + if slices.Contains(appliedMigrations, legacyVersion) { + l.WithField("legacy_version", legacyVersion).WithField("migration_table", mtn).Debug("Migration has already been applied in a legacy migration run. Updating version in migration table.") + if err := mb.isolatedTransaction(ctx, "init-migrate", func(conn *pop.Connection) error { + // We do not want to remove the legacy migration version or subsequent migrations might be applied twice. + // + // Do not activate the following - it is just for reference. + // + // if _, err := tx.Store.Exec(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), legacyVersion); err != nil { + // return errors.Wrapf(err, "problem removing legacy version %s", mi.Version) + // } + + // #nosec G201 - mtn is a system-wide const + err := conn.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec() + return errors.Wrapf(err, "problem inserting migration version %s", mi.Version) + }); err != nil { + return errors.WithStack(err) + } + continue + } + + l.Info("Migration has not yet been applied, running migration.") + + if err := mi.Valid(); err != nil { + return errors.WithStack(err) + } + + noTx := mb.shouldNotUseTransaction(mi) + if noTx { + l.Info("NOT running migrations inside a transaction") + if err := mi.Runner(mi, c); err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := c.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem inserting migration version %s. YOUR DATABASE MAY BE IN AN INCONSISTENT STATE! MANUAL INTERVENTION REQUIRED!", mi.Version) + } + } else { + if err := mb.isolatedTransaction(ctx, "up", func(conn *pop.Connection) error { + if err := mi.Runner(mi, conn); err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := conn.RawQuery(fmt.Sprintf("INSERT INTO %s (version) VALUES (?)", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem inserting migration version %s", mi.Version) + } + return nil + }); err != nil { + return errors.WithStack(err) + } + } + + l.WithField("autocommit", noTx).Infof("> %s applied successfully", mi.Name) + applied++ + if step > 0 && applied >= step { + break + } + } + if applied == 0 { + mb.l.Infof("Migrations already up to date, nothing to apply") + } else { + mb.l.Infof("Successfully applied %d migrations.", applied) + } + + // Save a SQLite template after applying all migrations (not for + // partial step runs) so future test runs can skip migrations. + // + // Only save when at least one migration was applied. Saving when + // applied == 0 either captures an empty DB or an already-migrated + // one that may contain test data inserted by an earlier registry + // (e.g. the Hydra janitor reuses the same DSN), which would + // pollute the shared template. + // + // Use VACUUM INTO to produce a clean, fully-checkpointed copy + // without a separate wal_checkpoint step. Write to a temp path + // unique to this test's database, then atomically rename to the + // shared template path. This prevents concurrent saves from + // multiple parallel tests from corrupting the template: + // os.Rename is atomic on the same filesystem, so the last writer + // wins with valid content and no reader ever sees a partial file. + if isOnDiskSQLite && step <= 0 && !mb.disableGoldenDatabase && applied > 0 { + templatePath := mb.sqliteTemplatePath() + tmp, err := os.CreateTemp(filepath.Dir(templatePath), ".sqlite-template-*.sqlite") + if err != nil { + mb.l.Errorf("Failed to allocate SQLite template file: err=%v", err) + return nil + } + tmpPath := tmp.Name() + if closeErr := tmp.Close(); closeErr != nil { + _ = os.Remove(tmpPath) + mb.l.Errorf("Failed to close SQLite template file: err=%v", closeErr) + return nil + } + // VACUUM INTO requires that the destination does not exist. + if removeErr := os.Remove(tmpPath); removeErr != nil { + mb.l.Errorf("Failed to prepare SQLite template file: err=%v", removeErr) + return nil + } + defer func() { _ = os.Remove(tmpPath) }() + // #nosec G201 - tmpPath is under os.TempDir() with a + // hex-hash filename, never derived from user input. + vacuumSQL := fmt.Sprintf("VACUUM INTO '%s'", strings.ReplaceAll(tmpPath, "'", "''")) + if err := mb.c.RawQuery(vacuumSQL).Exec(); err != nil { + mb.l.Errorf("Failed to create SQLite template copy: err=%v", err) + } else if err := os.Rename(tmpPath, templatePath); err != nil { + _ = os.Remove(tmpPath) + mb.l.Errorf("Failed to install SQLite template: src=%s dst=%s err=%v", tmpPath, templatePath, err) + } else { + mfs := mb.migrationsUp.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + mb.l.Infof("SQLite template saved, subsequent test runs will skip %d migrations: path=%s", len(mfs), templatePath) + } + } + return nil + }) + return applied, errors.WithStack(err) +} + +// Down runs pending "down" migrations and rolls back the +// database by the specified number of steps. +// If step <= 0, all down migrations are run. +func (mb *MigrationBox) Down(ctx context.Context, steps int) (err error) { + ctx, span := startSpan(ctx, MigrationDownOpName, trace.WithAttributes(attribute.Int("steps", steps))) + defer otelx.End(span, &err) + + if steps <= 0 { + steps = math.MaxInt + } + remainingSteps := steps + + c := mb.c.WithContext(ctx) + return errors.WithStack(mb.exec(ctx, func() (err error) { + mtn := sanitizedMigrationTableName(c) + count, err := c.Count(mtn) + if err != nil { + return errors.Wrap(err, "migration down: unable count existing migration") + } + attemptSteps := min(remainingSteps, count) + + mfs := mb.migrationsDown.sortAndFilter(mb.c.Dialect.Name(), mb.migrationFallbacks()...) + slices.Reverse(mfs) + if len(mfs) > count { + // skip all migrations that were not yet applied + mfs = mfs[len(mfs)-count:] + } + + reverted := 0 + defer func() { + migrationsToRevertCount := min(attemptSteps, len(mfs)) + mb.l.Debugf("Successfully reverted %d/%d migrations.", reverted, migrationsToRevertCount) + if err != nil { + mb.l.WithError(err).Error("Problem reverting migrations.") + } + }() + for i, mi := range mfs { + if i >= attemptSteps { + break + } + l := mb.l.WithField("version", mi.Version).WithField("migration_name", mi.Name).WithField("migration_file", mi.Path) + l.Debugf("handling migration %s", mi.Name) + exists, err := c.Where("version = ?", mi.Version).Exists(mtn) + if err != nil { + return errors.Wrapf(err, "problem checking for migration version %s", mi.Version) + } + + if !exists && len(mi.Version) > 14 { + legacyVersion := mi.Version[:14] + legacyVersionExists, err := c.Where("version = ?", legacyVersion).Exists(mtn) + if err != nil { + return errors.Wrapf(err, "problem checking for legacy migration version %s", legacyVersion) + } + + if !legacyVersionExists { + return errors.Errorf("neither normal (%s) nor legacy migration (%s) exist", mi.Version, legacyVersion) + } + } else if !exists { + return errors.Errorf("migration version %s does not exist", mi.Version) + } + + if err := mi.Valid(); err != nil { + return errors.WithStack(err) + } + + if mb.shouldNotUseTransaction(mi) { + err := mi.Runner(mi, c) + if err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := c.RawQuery(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem deleting migration version %s. YOUR DATABASE MAY BE IN AN INCONSISTENT STATE! MANUAL INTERVENTION REQUIRED!", mi.Version) + } + } else { + if err := mb.isolatedTransaction(ctx, "down", func(conn *pop.Connection) error { + err := mi.Runner(mi, conn) + if err != nil { + return errors.WithStack(err) + } + + // #nosec G201 - mtn is a system-wide const + if err := conn.RawQuery(fmt.Sprintf("DELETE FROM %s WHERE version = ?", mtn), mi.Version).Exec(); err != nil { + return errors.Wrapf(err, "problem deleting migration version %s", mi.Version) + } + + return nil + }); err != nil { + return errors.WithStack(err) + } + } + + l.Infof("%s applied successfully", mi.Name) + reverted++ + remainingSteps-- + } + return nil + })) +} + +func (mb *MigrationBox) createTransactionalMigrationTable(ctx context.Context, c *pop.Connection, l *logrusx.Logger) error { + mtn := sanitizedMigrationTableName(c) + + if err := mb.createMigrationStatusTableTransaction(ctx, []string{ + fmt.Sprintf(`CREATE TABLE %s (version VARCHAR (48) NOT NULL, version_self INT NOT NULL DEFAULT 0)`, mtn), + fmt.Sprintf(`CREATE UNIQUE INDEX %s_version_idx ON %s (version)`, mtn, mtn), + fmt.Sprintf(`CREATE INDEX %s_version_self_idx ON %s (version_self)`, mtn, mtn), + }); err != nil { + return errors.WithStack(err) + } + + l.WithField("migration_table", mtn).Debug("Transactional migration table created successfully.") + + return nil +} + +func (mb *MigrationBox) migrateToTransactionalMigrationTable(ctx context.Context, c *pop.Connection, l *logrusx.Logger) error { + // This means the new pop migrator has also not yet been applied, do that now. + mtn := sanitizedMigrationTableName(c) + + withOn := fmt.Sprintf(" ON %s", mtn) + if c.Dialect.Name() != "mysql" { + withOn = "" + } + + interimTable := fmt.Sprintf("%s_transactional", mtn) + workload := [][]string{ + { + fmt.Sprintf(`DROP INDEX %s_version_idx%s`, mtn, withOn), + fmt.Sprintf(`CREATE TABLE %s (version VARCHAR (48) NOT NULL, version_self INT NOT NULL DEFAULT 0)`, interimTable), + fmt.Sprintf(`CREATE UNIQUE INDEX %s_version_idx ON %s (version)`, mtn, interimTable), + fmt.Sprintf(`CREATE INDEX %s_version_self_idx ON %s (version_self)`, mtn, interimTable), + // #nosec G201 - mtn is a system-wide const + fmt.Sprintf(`INSERT INTO %s (version) SELECT version FROM %s`, interimTable, mtn), + fmt.Sprintf(`ALTER TABLE %s RENAME TO %s_pop_legacy`, mtn, mtn), + }, + { + fmt.Sprintf(`ALTER TABLE %s RENAME TO %s`, interimTable, mtn), + }, + } + + if err := mb.createMigrationStatusTableTransaction(ctx, workload...); err != nil { + return errors.WithStack(err) + } + + l.WithField("migration_table", mtn).Debug("Successfully migrated legacy schema_migration to new transactional schema_migration table.") + + return nil +} + +func (mb *MigrationBox) isolatedTransaction(ctx context.Context, direction string, fn func(c *pop.Connection) error) (err error) { + ctx, span := startSpan(ctx, MigrationRunTransactionOpName, trace.WithAttributes(attribute.String("migration_direction", direction))) + defer otelx.End(span, &err) + + if mb.perMigrationTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeoutCause(ctx, mb.perMigrationTimeout, errors.Errorf("failed to run all SQL migrations: direction=%s timeout=%s", direction, mb.perMigrationTimeout)) + defer cancel() + } + + return Transaction(ctx, mb.c.WithContext(ctx), func(ctx context.Context, connection *pop.Connection) error { + return errors.WithStack(fn(connection)) + }) +} + +func (mb *MigrationBox) createMigrationStatusTableTransaction(ctx context.Context, transactions ...[]string) error { + for _, statements := range transactions { + // CockroachDB does not support transactional schema changes, so we have to run + // the statements outside of a transaction. The same applies to any dialect + // that opts into autocommit DDL (see noTxDDL). + if mb.noTxDDL() { + for _, statement := range statements { + if err := mb.c.WithContext(ctx).RawQuery(statement).Exec(); err != nil { + return errors.Wrapf(err, "unable to execute statement: %s", statement) + } + } + } else { + if err := mb.isolatedTransaction(ctx, "init", func(conn *pop.Connection) error { + for _, statement := range statements { + if err := conn.WithContext(ctx).RawQuery(statement).Exec(); err != nil { + return errors.Wrapf(err, "unable to execute statement: %s", statement) + } + } + return nil + }); err != nil { + return errors.WithStack(err) + } + } + } + + return nil +} + +// CreateSchemaMigrations sets up a table to track migrations. This is an idempotent +// operation. +func (mb *MigrationBox) CreateSchemaMigrations(ctx context.Context) error { + ctx, span := startSpan(ctx, MigrationInitOpName) + defer span.End() + + c := mb.c.WithContext(ctx) + + mtn := sanitizedMigrationTableName(c) + mb.l.WithField("migration_table", mtn).Debug("Checking if legacy migration table exists.") + _, err := c.Store.Exec(fmt.Sprintf("select version from %s", mtn)) + if err != nil { + mb.l.WithError(err).WithField("migration_table", mtn).Debug("An error occurred while checking for the legacy migration table, maybe it does not exist yet? Trying to create.") + // This means that the legacy pop migrator has not yet been applied + return errors.WithStack(mb.createTransactionalMigrationTable(ctx, c, mb.l)) + } + + mb.l.WithField("migration_table", mtn).Debug("A migration table exists, checking if it is a transactional migration table.") + _, err = c.Store.Exec(fmt.Sprintf("select version, version_self from %s", mtn)) + if err != nil { + mb.l.WithError(err).WithField("migration_table", mtn).Debug("An error occurred while checking for the transactional migration table, maybe it does not exist yet? Trying to create.") + return errors.WithStack(mb.migrateToTransactionalMigrationTable(ctx, c, mb.l)) + } + + mb.l.WithField("migration_table", mtn).Debug("Migration tables exist and are up to date.") + return nil +} + +type MigrationStatus struct { + State string `json:"state"` + Version string `json:"version"` + Name string `json:"name"` + ContentUp string `json:"content"` + ContentDown string `json:"content_down"` +} + +type MigrationStatuses []MigrationStatus + +var _ cmdx.Table = (MigrationStatuses)(nil) + +func (m MigrationStatuses) Header() []string { + return []string{"Version", "Name", "Status"} +} + +func (m MigrationStatuses) Table() [][]string { + t := make([][]string, len(m)) + for i, s := range m { + t[i] = []string{s.Version, s.Name, s.State} + } + return t +} + +func (m MigrationStatuses) Interface() interface{} { + return m +} + +func (m MigrationStatuses) Len() int { + return len(m) +} + +func (m MigrationStatuses) IDs() []string { + ids := make([]string, len(m)) + for i, s := range m { + ids[i] = s.Version + } + return ids +} + +func (m MigrationStatuses) HasPending() bool { + for _, mm := range m { + if mm.State == Pending { + return true + } + } + return false +} + +func sanitizedMigrationTableName(con *pop.Connection) string { + return regexp.MustCompile(`\W`).ReplaceAllString(con.MigrationTableName(), "") +} + +func errIsTableNotFound(err error) bool { + return strings.Contains(err.Error(), "no such table:") || // sqlite + strings.Contains(err.Error(), "Error 1146") || // MySQL + strings.Contains(err.Error(), "SQLSTATE 42P01") // PostgreSQL / CockroachDB +} + +// Status prints out the status of applied/pending migrations. +func (mb *MigrationBox) Status(ctx context.Context) (MigrationStatuses, error) { + ctx, span := startSpan(ctx, MigrationStatusOpName) + defer span.End() + + con := mb.c.WithContext(ctx) + + dialect := mb.c.Dialect.Name() + fallbacks := mb.migrationFallbacks() + migrationsUp := mb.migrationsUp.sortAndFilter(dialect, fallbacks...) + + if len(migrationsUp) == 0 { + return nil, errors.Errorf("unable to find any migrations for dialect: %s", dialect) + } + + alreadyApplied := make([]string, 0, len(migrationsUp)) + err := con.RawQuery(fmt.Sprintf("SELECT version FROM %s", sanitizedMigrationTableName(con))).All(&alreadyApplied) + if err != nil { + if errIsTableNotFound(err) { + // This means that no migrations have been applied and we need to apply all of them first! + // + // It also means that we can ignore this state and act as if no migrations have been applied yet. + } else { + // On any other error, we fail. + return nil, errors.Wrap(err, "problem with migration") + } + } + + statuses := make(MigrationStatuses, len(migrationsUp)) + for k, mf := range migrationsUp { + downContent := "-- error: no down migration defined for this migration" + if mDown := mb.migrationsDown.find(mf.Version, dialect, fallbacks...); mDown != nil { + downContent = mDown.Content + } + statuses[k] = MigrationStatus{ + State: Pending, + Version: mf.Version, + Name: mf.Name, + ContentUp: mf.Content, + ContentDown: downContent, + } + + if slices.ContainsFunc(alreadyApplied, func(applied string) bool { + return applied == mf.Version || (len(mf.Version) > 14 && applied == mf.Version[:14]) + }) { + statuses[k].State = Applied + continue + } + } + + return statuses, nil +} + +// DumpMigrationSchema will generate a file of the current database schema +func (mb *MigrationBox) DumpMigrationSchema(ctx context.Context) error { + c := mb.c.WithContext(ctx) + schema := "schema.sql" + f, err := os.Create(schema) //#nosec:G304 + if err != nil { + return errors.WithStack(err) + } + err = c.Dialect.DumpSchema(f) + if err != nil { + _ = os.RemoveAll(schema) + return errors.WithStack(err) + } + return nil +} + +func (mb *MigrationBox) exec(ctx context.Context, fn func() error) error { + now := time.Now() + defer mb.printTimer(now) + + err := mb.CreateSchemaMigrations(ctx) + if err != nil { + return errors.Wrap(err, "migrator: problem creating schema migrations") + } + + if mb.c.Dialect.Name() == "sqlite3" { + if err := mb.c.RawQuery("PRAGMA foreign_keys=OFF").Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + + switch mb.c.Dialect.Name() { + case dbal.DriverCockroachDB, dbal.DriverYugabyteDB: + outer := fn + fn = func() error { + // CreateSchemaMigrations runs before this wrapper. YugabyteDB uses + // autocommit DDL, but its pgwire errors expose the same retryable + // SQLSTATEs that crdb.Execute classifies for whole-run retries. + return errors.WithStack(crdb.Execute(outer)) + } + } + + if err := fn(); err != nil { + return errors.WithStack(err) + } + + if mb.c.Dialect.Name() == "sqlite3" { + if err := mb.c.RawQuery("PRAGMA foreign_keys=ON").Exec(); err != nil { + return sqlcon.HandleError(err) + } + } + + return nil +} + +func (mb *MigrationBox) printTimer(timerStart time.Time) { + diff := time.Since(timerStart).Seconds() + if diff > 60 { + mb.l.Debugf("%.4f minutes", diff/60) + } else { + mb.l.Debugf("%.4f seconds", diff) + } +} diff --git a/oryx/popx/span.go b/oryx/popx/span.go new file mode 100644 index 0000000..9aef3c6 --- /dev/null +++ b/oryx/popx/span.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + + "go.opentelemetry.io/otel/trace" +) + +const ( + MigrationStatusOpName = "migration-status" + MigrationInitOpName = "migration-init" + MigrationUpOpName = "migration-up" + MigrationRunTransactionOpName = "migration-run-transaction" + MigrationDownOpName = "migration-down" +) + +func startSpan(ctx context.Context, opName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return trace.SpanFromContext(ctx).TracerProvider().Tracer(tracingComponent).Start(ctx, opName, opts...) +} diff --git a/oryx/popx/sql_template_funcs.go b/oryx/popx/sql_template_funcs.go new file mode 100644 index 0000000..32aa24f --- /dev/null +++ b/oryx/popx/sql_template_funcs.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "regexp" + + "github.com/pkg/errors" +) + +var SQLTemplateFuncs = map[string]interface{}{ + "identifier": Identifier, +} + +var identifierPattern = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_]*$") + +func Identifier(i string) (string, error) { + if !identifierPattern.MatchString(i) { + return "", errors.Errorf("invalid SQL identifier '%s'", i) + } + return i, nil +} diff --git a/oryx/popx/sqlite_template.go b/oryx/popx/sqlite_template.go new file mode 100644 index 0000000..56ebb22 --- /dev/null +++ b/oryx/popx/sqlite_template.go @@ -0,0 +1,88 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + "github.com/ory/x/logrusx" +) + +// sqliteFilePath returns the absolute file path for an on-disk SQLite DSN. +// SQLite memory databases and non-SQLite DSNs return ("", false). +func sqliteFilePath(dsn string) (string, bool) { + rest := dsn + if scheme, afterScheme, ok := strings.Cut(dsn, "://"); ok { + if scheme != "sqlite" && scheme != "sqlite3" { + return "", false + } + rest = afterScheme + } + if strings.Contains(rest, ":memory:") || strings.Contains(rest, "mode=memory") { + return "", false + } + rest = strings.TrimPrefix(rest, "file:") + if i := strings.IndexByte(rest, '?'); i >= 0 { + rest = rest[:i] + } + if !filepath.IsAbs(rest) { + return "", false + } + return rest, true +} + +// PrepareTestSQLiteDatabase migrates a fresh on-disk SQLite test database +// before the application opens it. MigrationBox's shared template cache makes +// this a cheap template restore after the first migration set has been built. +// +// This is useful when an application normally applies multiple migration sets +// in separate passes: pass their merged filesystem here so the cached template +// represents the final combined schema. Non-SQLite DSNs, memory databases, and +// existing database files are left untouched. +func PrepareTestSQLiteDatabase( + ctx context.Context, + dsn string, + migrations fs.FS, + logger *logrusx.Logger, + opts ...MigrationBoxOption, +) error { + if !testing.Testing() { + return nil + } + path, ok := sqliteFilePath(dsn) + if !ok { + return nil + } + if _, err := os.Stat(path); err == nil { + return nil + } else if !errors.Is(err, os.ErrNotExist) { + return errors.WithStack(err) + } + + c, err := pop.NewConnection(&pop.ConnectionDetails{URL: dsn}) + if err != nil { + return errors.WithStack(err) + } + if err := c.Open(); err != nil { + return errors.WithStack(err) + } + + mb, err := NewMigrationBox(migrations, c, logger, opts...) + if err == nil { + err = mb.Up(ctx) + } + closeErr := c.Close() + if err != nil { + return errors.WithStack(err) + } + return errors.WithStack(closeErr) +} diff --git a/oryx/popx/stub/migrations/check/valid/123_a.down.sql b/oryx/popx/stub/migrations/check/valid/123_a.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/check/valid/123_a.mysql.up.sql b/oryx/popx/stub/migrations/check/valid/123_a.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/check/valid/123_a.postgres.up.sql b/oryx/popx/stub/migrations/check/valid/123_a.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql new file mode 100644 index 0000000..30e62aa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credentials";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_credential_types";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identities";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql new file mode 100644 index 0000000..638b1b7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.cockroach.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql new file mode 100644 index 0000000..fcf2432 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_credential_identifiers`; +DROP TABLE `identity_credentials`; +DROP TABLE `identity_credential_types`; +DROP TABLE `identities`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql new file mode 100644 index 0000000..35d1c2a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.mysql.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`); +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql new file mode 100644 index 0000000..fec4915 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.postgres.up.sql @@ -0,0 +1,35 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..923da20 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_credential_identifiers"; +DROP TABLE "identity_credentials"; +DROP TABLE "identity_credential_types"; +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..bf912cb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000001_identities.sqlite3.up.sql @@ -0,0 +1,31 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +); +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name); +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +); +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql new file mode 100644 index 0000000..14ba150 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql new file mode 100644 index 0000000..1bca145 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.cockroach.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql new file mode 100644 index 0000000..8aac48a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.down.sql @@ -0,0 +1,5 @@ +DROP TABLE `selfservice_login_request_methods`; +DROP TABLE `selfservice_login_requests`; +DROP TABLE `selfservice_registration_request_methods`; +DROP TABLE `selfservice_registration_requests`; +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql new file mode 100644 index 0000000..9894e39 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.mysql.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql new file mode 100644 index 0000000..d24b8b0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.postgres.up.sql @@ -0,0 +1,55 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..356a2e6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.down.sql @@ -0,0 +1,5 @@ +DROP TABLE "selfservice_login_request_methods"; +DROP TABLE "selfservice_login_requests"; +DROP TABLE "selfservice_registration_request_methods"; +DROP TABLE "selfservice_registration_requests"; +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..d265527 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000002_requests.sqlite3.up.sql @@ -0,0 +1,50 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +); +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql new file mode 100644 index 0000000..b7ffdf0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql new file mode 100644 index 0000000..c2b8ea0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000003_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql new file mode 100644 index 0000000..3081431 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql new file mode 100644 index 0000000..4e6d1a9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000004_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql new file mode 100644 index 0000000..139e50a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); diff --git a/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql new file mode 100644 index 0000000..8069ee9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000005_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql new file mode 100644 index 0000000..efa6f4e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql new file mode 100644 index 0000000..5b10f19 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000006_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql new file mode 100644 index 0000000..3b38079 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql new file mode 100644 index 0000000..434d2ef --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT '';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql new file mode 100644 index 0000000..af1b234 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors"; + +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000007_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..ba30655 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_verifiable_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..5e030a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.cockroach.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..2e080c8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.down.sql @@ -0,0 +1,2 @@ +DROP TABLE `selfservice_verification_requests`; +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..5951c67 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.mysql.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`); +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..419ec91 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.postgres.up.sql @@ -0,0 +1,32 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..593423b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.down.sql @@ -0,0 +1,2 @@ +DROP TABLE "selfservice_verification_requests"; +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..a12f20a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000008_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,30 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value); +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql new file mode 100644 index 0000000..f8a7e0f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); diff --git a/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000009_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql new file mode 100644 index 0000000..c42e3bd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.down.sql @@ -0,0 +1,5 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql new file mode 100644 index 0000000..e9b8fc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql new file mode 100644 index 0000000..525b179 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql new file mode 100644 index 0000000..ccf29d7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.down.sql @@ -0,0 +1,2 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql new file mode 100644 index 0000000..52ef938 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.down.sql @@ -0,0 +1,13 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL; +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql new file mode 100644 index 0000000..fc59202 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000010_errors.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +); +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors"; +DROP TABLE "selfservice_errors"; +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..53dd25f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ADD COLUMN "body" text;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "courier_messages" SET "body" = "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..abd09ec --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000011_courier_body_type.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages"; +DROP TABLE "courier_messages"; +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..f6ee1d9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..56056e9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..a4db2cc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20191100000012_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..aa1ddb2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..5601793 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..d2e92cd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON; +DROP TABLE `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..1c15ee7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..a800a7b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb; +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t); +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e5b9297 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32); +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..c1e707f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP TABLE "selfservice_profile_management_request_methods"; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..f8c1bd8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200317160354_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,26 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT; +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests; +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests"; + +DROP TABLE "selfservice_profile_management_requests"; +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..11b99bc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..36a86ae --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200401183443_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..2d7a810 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..90b3c60 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..dba65df --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..7d66d48 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..d37a192 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..7d75607 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200402142539_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..32088d9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "identity_recovery_addresses";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..f6c8b50 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..888e504 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.down.sql @@ -0,0 +1,4 @@ +DROP TABLE `identity_recovery_tokens`; +DROP TABLE `selfservice_recovery_request_methods`; +DROP TABLE `selfservice_recovery_requests`; +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..a935381 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`); +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`); +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..5b3fa1c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,52 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..b474724 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1,4 @@ +DROP TABLE "identity_recovery_tokens"; +DROP TABLE "selfservice_recovery_request_methods"; +DROP TABLE "selfservice_recovery_requests"; +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..2901619 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101057_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,48 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value); +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value); +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +); +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..54c99e1 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); diff --git a/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..7972b34 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200519101058_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..35028f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..127f682 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..346124a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.down.sql @@ -0,0 +1,16 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql new file mode 100644 index 0000000..3bf20de --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200601101001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql new file mode 100644 index 0000000..80601b5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql new file mode 100644 index 0000000..a9cfe75 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql new file mode 100644 index 0000000..427a387 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql new file mode 100644 index 0000000..21085a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql new file mode 100644 index 0000000..7c73915 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.down.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql new file mode 100644 index 0000000..4ac597f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql new file mode 100644 index 0000000..e1d0c11 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.down.sql @@ -0,0 +1,44 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql new file mode 100644 index 0000000..cc2a606 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200605111551_messages.sqlite3.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql new file mode 100644 index 0000000..5877dc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql new file mode 100644 index 0000000..5848a58 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.cockroach.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql new file mode 100644 index 0000000..c43477e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state`; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql new file mode 100644 index 0000000..00b29fa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.mysql.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql new file mode 100644 index 0000000..eaee899 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql new file mode 100644 index 0000000..4c2ac98 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.postgres.up.sql @@ -0,0 +1,2 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql new file mode 100644 index 0000000..7366ef2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.down.sql @@ -0,0 +1,17 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql new file mode 100644 index 0000000..3892e62 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200607165100_settings.sqlite3.up.sql @@ -0,0 +1,18 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..07a1c56 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..fc35d52 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200705105359_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql new file mode 100644 index 0000000..10fcdad --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql new file mode 100644 index 0000000..b1edbc9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.cockroach.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql new file mode 100644 index 0000000..6d503a7 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type`; +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql new file mode 100644 index 0000000..2953d00 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.mysql.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql new file mode 100644 index 0000000..e36b97a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type"; +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql new file mode 100644 index 0000000..74d85bc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.postgres.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..238eea6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.down.sql @@ -0,0 +1,82 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +); +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests"; + +DROP TABLE "selfservice_login_requests"; +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +); +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests"; + +DROP TABLE "selfservice_registration_requests"; +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests"; + +DROP TABLE "selfservice_settings_requests"; +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at FROM "selfservice_recovery_requests"; + +DROP TABLE "selfservice_recovery_requests"; +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests"; +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +); +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success FROM "selfservice_verification_requests"; + +DROP TABLE "selfservice_verification_requests"; +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..85a72ea --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810141652_flow_type.sqlite3.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..7b665f0 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..9b60677 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.cockroach.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql new file mode 100644 index 0000000..6ba6c69 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests`; +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods`; +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods`; +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests`; +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods`; +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests`; +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods`; +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests`; +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql new file mode 100644 index 0000000..28508f8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.mysql.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods`; +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows`; +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods`; +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows`; +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods`; +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows`; +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods`; +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows`; +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.postgres.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..60d6d0d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.down.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests"; +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods"; +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods"; +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods"; +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests"; +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..be3be9e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810161022_flow_rename.sqlite3.up.sql @@ -0,0 +1,9 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods"; +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows"; +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods"; +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows"; +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods"; +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows"; +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods"; +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows"; +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..0ccaced --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..9f7efff --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.cockroach.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..9a54733 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..93844b8 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.mysql.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.postgres.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..931cf71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..9f351a5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200810162450_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id"; +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id"; +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..f9e4897 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..2549800 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.cockroach.up.sql @@ -0,0 +1,8 @@ +DELETE FROM sessions;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "sessions" SET "token" = "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "sessions" DROP COLUMN "_token_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "sessions_token_idx" ON "sessions" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql new file mode 100644 index 0000000..fed24fb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.mysql.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions; +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32); +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32); +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`); +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql new file mode 100644 index 0000000..77edf55 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.postgres.up.sql @@ -0,0 +1,5 @@ +DELETE FROM sessions; +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32); +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..75b3650 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.down.sql @@ -0,0 +1,16 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +DROP INDEX IF EXISTS "sessions_token_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..a9f847a --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812124254_add_session_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +DELETE FROM sessions; +ALTER TABLE "sessions" ADD COLUMN "token" TEXT; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token); +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..f51c199 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..cc90f31 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9e7bfcb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,19 @@ +DROP INDEX IF EXISTS "sessions_token_idx"; +DROP INDEX IF EXISTS "sessions_token_uq_idx"; +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token); +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token); +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions"; + +DROP TABLE "sessions"; +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200812160551_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..180633b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..b254b6e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830121710_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..25bc849 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}';COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP TABLE "selfservice_verification_flow_methods";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..c325dcb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form';COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..fd97b4c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON; +DROP TABLE `selfservice_verification_flow_methods`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state`; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..adbf65d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb; +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t); +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL; +DROP TABLE "selfservice_verification_flow_methods"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..27a7bfa --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,34 @@ +DROP TABLE "selfservice_verification_flow_methods"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser' +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email'; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130642_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130643_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..34053ed --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..2dfbfde --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e8761c6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6677fbd --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130644_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130645_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..fdccf04 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.cockroach.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..496f1eb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via`; +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..3aa5b8e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via"; +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..db1391d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830130646_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,54 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +); +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows"; + +DROP TABLE "selfservice_verification_flows"; +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..374a2cf --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..12d59e2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.cockroach.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..6050119 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.mysql.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB; +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`); +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`); +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..a171839 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.postgres.up.sql @@ -0,0 +1,19 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..9250c46 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830154602_add_verification_token.sqlite3.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +); +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token); +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id); +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..9a992a2 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.down.sql @@ -0,0 +1,10 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..adb3e1d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.cockroach.up.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00';COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..696bde3 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at`; +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e5f6e9d --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.mysql.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..38e7b8b --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.down.sql @@ -0,0 +1,4 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at"; +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..79c2e87 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.postgres.up.sql @@ -0,0 +1,3 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ea864bb --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,63 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at FROM "identity_recovery_tokens"; + +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..7ad4a61 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200830172221_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,23 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00'; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx"; +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token); +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token); +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens"; +DROP TABLE "identity_recovery_tokens"; +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..932fd71 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1,15 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32);COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL;COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp;COMMIT TRANSACTION;BEGIN TRANSACTION; +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp";COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code);COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..24a61df --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1,4 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code";COMMIT TRANSACTION;BEGIN TRANSACTION; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at";COMMIT TRANSACTION;BEGIN TRANSACTION; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..9cb6530 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32); +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME; +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL; +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME; +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`); +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..91dafe3 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1,4 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses`; +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code`; +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..6425e54 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1,8 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32); +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp; +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL; +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..840985e --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1,4 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX "identity_verifiable_addresses_code_idx"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code"; +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f79f141 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,48 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT; +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME; +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL; +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses"; +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code); +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..1279dc6 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20200831110752_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,43 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx"; +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx"; +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +); +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value); +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses"; + +DROP TABLE "identity_verifiable_addresses"; +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.cockroach.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.mysql.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.postgres.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..ec08e32 --- /dev/null +++ b/oryx/popx/stub/migrations/legacy/20201201161451_credential_types_values.sqlite3.up.sql @@ -0,0 +1,2 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password'); +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql new file mode 100644 index 0000000..8ea7e8f --- /dev/null +++ b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.down.sql @@ -0,0 +1 @@ +BEGIN;ROLLBACK; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql new file mode 100644 index 0000000..8ea7e8f --- /dev/null +++ b/oryx/popx/stub/migrations/notx/20241031_notx.autocommit.up.sql @@ -0,0 +1 @@ +BEGIN;ROLLBACK; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz b/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz new file mode 100644 index 0000000..149f0fd --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000001_identities.down.fizz @@ -0,0 +1,4 @@ +drop_table("identity_credential_identifiers") +drop_table("identity_credentials") +drop_table("identity_credential_types") +drop_table("identities") diff --git a/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz b/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz new file mode 100644 index 0000000..ee11525 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000001_identities.up.fizz @@ -0,0 +1,34 @@ +create_table("identities") { + t.Column("id", "uuid", {primary: true}) + t.Column("traits_schema_id", "string", {"size": 2048}) + t.Column("traits", "json") +} + +create_table("identity_credential_types") { + t.Column("id", "uuid", {primary: true}) + t.Column("name", "string", { "size": 32 }) + + t.DisableTimestamps() +} + +add_index("identity_credential_types", "name", {"unique": true}) + +create_table("identity_credentials") { + t.Column("id", "uuid", {primary: true}) + t.Column("config", "json") + + t.Column("identity_credential_type_id", "uuid") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) + t.ForeignKey("identity_credential_type_id", {"identity_credential_types": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("identity_credential_identifiers") { + t.Column("id", "uuid", {primary: true}) + t.Column("identifier", "string", {"size": 255}) + t.Column("identity_credential_id", "uuid") + t.ForeignKey("identity_credential_id", {"identity_credentials": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_credential_identifiers", "identifier", {"unique": true}) diff --git a/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz b/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz new file mode 100644 index 0000000..d8a2fe2 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000002_requests.down.fizz @@ -0,0 +1,7 @@ +drop_table("selfservice_login_request_methods") +drop_table("selfservice_login_requests") + +drop_table("selfservice_registration_request_methods") +drop_table("selfservice_registration_requests") + +drop_table("selfservice_profile_management_requests") diff --git a/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz b/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz new file mode 100644 index 0000000..2823b61 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000002_requests.up.fizz @@ -0,0 +1,47 @@ +create_table("selfservice_login_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + t.Column("csrf_token", "string") +} + +create_table("selfservice_login_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_login_request_id", "uuid") + t.Column("config", "json") + + t.ForeignKey("selfservice_login_request_id", {"selfservice_login_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_registration_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + t.Column("csrf_token", "string") +} + +create_table("selfservice_registration_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_registration_request_id", "uuid") + t.Column("config", "json") + + t.ForeignKey("selfservice_registration_request_id", {"selfservice_registration_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_profile_management_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("form", "json") + t.Column("update_successful", "bool") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz b/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz new file mode 100644 index 0000000..dc5c982 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000003_sessions.down.fizz @@ -0,0 +1 @@ +drop_table("sessions") diff --git a/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz b/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz new file mode 100644 index 0000000..f0eb2f3 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000003_sessions.up.fizz @@ -0,0 +1,9 @@ +create_table("sessions") { + t.Column("id", "uuid", {primary: true}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("authenticated_at", "timestamp") + t.Column("identity_id", "uuid") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz new file mode 100644 index 0000000..9ada90a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000004_errors.down.fizz @@ -0,0 +1 @@ +drop_table("selfservice_errors") diff --git a/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz new file mode 100644 index 0000000..2911b5e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000004_errors.up.fizz @@ -0,0 +1,6 @@ +create_table("selfservice_errors") { + t.Column("id", "uuid", {primary: true}) + t.Column("errors", "json") + t.Column("seen_at", "timestamp") + t.Column("was_seen", "bool") +} diff --git a/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql new file mode 100644 index 0000000..139e50a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255); diff --git a/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql new file mode 100644 index 0000000..8069ee9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000005_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz b/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz new file mode 100644 index 0000000..2da9c63 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000006_courier.down.fizz @@ -0,0 +1 @@ +drop_table("courier_messages") diff --git a/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz b/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz new file mode 100644 index 0000000..5f6fda1 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000006_courier.up.fizz @@ -0,0 +1,10 @@ +create_table("courier_messages") { + t.Column("id", "uuid", {primary: true}) + + t.Column("type", "int") + t.Column("status", "int") + + t.Column("body", "string") + t.Column("subject", "string") + t.Column("recipient", "string") +} diff --git a/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz new file mode 100644 index 0000000..6f093e7 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000007_errors.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_errors", "csrf_token") diff --git a/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz new file mode 100644 index 0000000..b5aa72a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000007_errors.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_errors", "csrf_token", "string", {"default": ""}) diff --git a/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz new file mode 100644 index 0000000..48fd423 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.down.fizz @@ -0,0 +1,2 @@ +drop_table("selfservice_verification_requests") +drop_table("identity_verifiable_addresses") diff --git a/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz new file mode 100644 index 0000000..3f58ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000008_selfservice_verification.up.fizz @@ -0,0 +1,35 @@ +create_table("identity_verifiable_addresses") { + t.Column("id", "uuid", {primary: true}) + + t.Column("code", "string", {"size": 32}) + t.Column("status", "string", {"size": 16}) + t.Column("via", "string", {"size": 16}) + t.Column("verified", "bool") + + t.Column("value", "string", {"size": 400}) + + t.Column("verified_at", "timestamp", {"null": true}) + t.Column("expires_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + + t.Column("identity_id", "uuid") + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_verifiable_addresses", ["code"], { "unique": true, "name": "identity_verifiable_addresses_code_uq_idx" }) +add_index("identity_verifiable_addresses", ["code"], { "name": "identity_verifiable_addresses_code_idx" }) + +add_index("identity_verifiable_addresses", ["via", "value"], { "unique": true, "name": "identity_verifiable_addresses_status_via_uq_idx" }) +add_index("identity_verifiable_addresses", ["via", "value"], { "name": "identity_verifiable_addresses_status_via_idx" }) + +create_table("selfservice_verification_requests") { + t.Column("id", "uuid", {primary: true}) + + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + + t.Column("form", "json") + t.Column("via", "string", {"size": 16}) + t.Column("csrf_token", "string") + t.Column("success", "bool") +} diff --git a/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql new file mode 100644 index 0000000..f8a7e0f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255); diff --git a/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000009_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz b/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz new file mode 100644 index 0000000..daaf4c0 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000010_errors.down.fizz @@ -0,0 +1,2 @@ +sql("UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL;") +change_column("selfservice_errors", "seen_at", "timestamp", { null: false }) diff --git a/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz b/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz new file mode 100644 index 0000000..542c123 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000010_errors.up.fizz @@ -0,0 +1 @@ +change_column("selfservice_errors", "seen_at", "timestamp", { "null": true }) diff --git a/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz new file mode 100644 index 0000000..178c60c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.down.fizz @@ -0,0 +1,7 @@ +<%# + +Do nothing because the change will not be able to preserve data and the change is insignificant as it's compatible +with both code bases (prior and after this change). + +WARNING: https://github.com/gobuffalo/fizz/issues/45#issuecomment-586833728 +%> diff --git a/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz new file mode 100644 index 0000000..3ca90e2 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000011_courier_body_type.up.fizz @@ -0,0 +1 @@ +change_column("courier_messages", "body", "text", {}) diff --git a/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz new file mode 100644 index 0000000..43e866f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_login_requests", "forced") diff --git a/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz new file mode 100644 index 0000000..66fcd59 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20191100000012_login_request_forced.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_login_requests", "forced", "bool", {"default": false}) diff --git a/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz new file mode 100644 index 0000000..946a0d6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.down.fizz @@ -0,0 +1,12 @@ +{{ if or .IsPostgreSQL .IsMySQL .IsMariaDB }} + add_column("selfservice_profile_management_requests", "form", "json", { "null": true }) + sql("UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t);") + change_column("selfservice_profile_management_requests", "form", "json", { "null": false }) +{{ end }} + +{{ if .IsCockroach }} + add_column("selfservice_profile_management_requests", "form", "json", { "default": "{}" }) +{{ end }} + +drop_table("selfservice_profile_management_request_methods") +drop_column("selfservice_profile_management_requests", "active_method") diff --git a/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz new file mode 100644 index 0000000..276dca6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200317160354_create_profile_request_forms.up.fizz @@ -0,0 +1,12 @@ +create_table("selfservice_profile_management_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_profile_management_request_id", "uuid") + t.Column("config", "json") +} + +add_column("selfservice_profile_management_requests", "active_method", "string", {"size": 32, null: true}) + +sql("INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests;") + +drop_column("selfservice_profile_management_requests", "form") diff --git a/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz new file mode 100644 index 0000000..956151d --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.down.fizz @@ -0,0 +1 @@ +drop_table("continuity_containers") diff --git a/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz new file mode 100644 index 0000000..efaff42 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200401183443_continuity_containers.up.fizz @@ -0,0 +1,11 @@ +create_table("continuity_containers") { + t.Column("id", "uuid", {primary: true}) + + t.Column("identity_id", "uuid", {null: true}) + + t.Column("name", "string") + t.Column("payload", "json", {null: true}) + t.Column("expires_at", "timestamp") + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz new file mode 100644 index 0000000..cbf6f98 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.down.fizz @@ -0,0 +1,5 @@ +rename_column("selfservice_settings_request_methods", "selfservice_settings_request_id", "selfservice_profile_management_request_id") + +rename_table("selfservice_settings_request_methods", "selfservice_profile_management_request_methods") +rename_table("selfservice_settings_requests", "selfservice_profile_management_requests") + diff --git a/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz new file mode 100644 index 0000000..4b0132b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200402142539_rename_profile_flows.up.fizz @@ -0,0 +1,4 @@ +rename_column("selfservice_profile_management_request_methods", "selfservice_profile_management_request_id", "selfservice_settings_request_id") + +rename_table("selfservice_profile_management_request_methods", "selfservice_settings_request_methods") +rename_table("selfservice_profile_management_requests", "selfservice_settings_requests") diff --git a/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz new file mode 100644 index 0000000..04b5fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.down.fizz @@ -0,0 +1,4 @@ +drop_table("identity_recovery_tokens") +drop_table("selfservice_recovery_request_methods") +drop_table("selfservice_recovery_requests") +drop_table("identity_recovery_addresses") diff --git a/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz new file mode 100644 index 0000000..b037114 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101057_create_recovery_addresses.up.fizz @@ -0,0 +1,52 @@ +create_table("identity_recovery_addresses") { + t.Column("id", "uuid", {primary: true}) + + t.Column("via", "string", {"size": 16}) + t.Column("value", "string", {"size": 400}) + + t.Column("identity_id", "uuid") + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_recovery_addresses", ["via", "value"], { "unique": true, "name": "identity_recovery_addresses_status_via_uq_idx" }) +add_index("identity_recovery_addresses", ["via", "value"], { "name": "identity_recovery_addresses_status_via_idx" }) + +create_table("selfservice_recovery_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { "default_raw": "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("messages", "json", {"null": true}) + t.Column("active_method", "string", {"size": 32, "null": true}) + t.Column("csrf_token", "string") + t.Column("state", "string", {"size": 32}) + + t.Column("recovered_identity_id", "uuid", { "null": true }) + t.ForeignKey("recovered_identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_recovery_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("config", "json") + + t.Column("selfservice_recovery_request_id", "uuid") + t.ForeignKey("selfservice_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("identity_recovery_tokens") { + t.Column("id", "uuid", {primary: true}) + + t.Column("token", "string", {"size": 64}) + t.Column("used", "bool", {"default": false}) + t.Column("used_at", "timestamp", {"null": true}) + + t.Column("identity_recovery_address_id", "uuid") + t.ForeignKey("identity_recovery_address_id", {"identity_recovery_addresses": ["id"]}, {"on_delete": "cascade"}) + + t.Column("selfservice_recovery_request_id", "uuid") + t.ForeignKey("selfservice_recovery_request_id", {"selfservice_recovery_requests": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_recovery_tokens", ["token"], { "unique": true, "name": "identity_recovery_addresses_code_uq_idx" }) +add_index("identity_recovery_tokens", ["token"], { "name": "identity_recovery_addresses_code_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..54c99e1 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64); diff --git a/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..7972b34 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200519101058_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz b/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz new file mode 100644 index 0000000..602d6ec --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101000_create_messages.down.fizz @@ -0,0 +1 @@ +drop_column("selfservice_settings_requests", "messages") diff --git a/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz b/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz new file mode 100644 index 0000000..a4e0d5f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101000_create_messages.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_settings_requests", "messages", "json", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql new file mode 100644 index 0000000..d16bc78 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql new file mode 100644 index 0000000..3bf20de --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200601101001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY; diff --git a/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz b/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz new file mode 100644 index 0000000..81d91db --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200605111551_messages.down.fizz @@ -0,0 +1,3 @@ +drop_column("selfservice_verification_requests", "messages") +drop_column("selfservice_login_requests", "messages") +drop_column("selfservice_registration_requests", "messages") diff --git a/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz b/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz new file mode 100644 index 0000000..23704c0 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200605111551_messages.up.fizz @@ -0,0 +1,3 @@ +add_column("selfservice_verification_requests", "messages", "json", {"null": true}) +add_column("selfservice_login_requests", "messages", "json", {"null": true}) +add_column("selfservice_registration_requests", "messages", "json", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz b/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz new file mode 100644 index 0000000..89b26ed --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200607165100_settings.down.fizz @@ -0,0 +1,2 @@ +drop_column("selfservice_settings_requests", "state") +add_column("selfservice_settings_requests", "update_successful", "bool", {"default": false}) diff --git a/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz b/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz new file mode 100644 index 0000000..c7f3607 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200607165100_settings.up.fizz @@ -0,0 +1,2 @@ +add_column("selfservice_settings_requests", "state", "string", {"default": "show_form"}) +drop_column("selfservice_settings_requests", "update_successful") diff --git a/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz new file mode 100644 index 0000000..ed0715f --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.down.fizz @@ -0,0 +1 @@ +rename_column("identities", "schema_id", "traits_schema_id") diff --git a/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz new file mode 100644 index 0000000..5a9159b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200705105359_rename_identities_schema.up.fizz @@ -0,0 +1 @@ +rename_column("identities", "traits_schema_id", "schema_id") diff --git a/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz b/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz new file mode 100644 index 0000000..eee4e4e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810141652_flow_type.down.fizz @@ -0,0 +1,5 @@ +drop_column("selfservice_login_requests", "type") +drop_column("selfservice_registration_requests", "type") +drop_column("selfservice_settings_requests", "type") +drop_column("selfservice_recovery_requests", "type") +drop_column("selfservice_verification_requests", "type") diff --git a/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz b/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz new file mode 100644 index 0000000..90c2a76 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810141652_flow_type.up.fizz @@ -0,0 +1,5 @@ +add_column("selfservice_login_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_registration_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_settings_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_recovery_requests", "type", "string", {"default": "browser", "size": 16}) +add_column("selfservice_verification_requests", "type", "string", {"default": "browser", "size": 16}) diff --git a/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz new file mode 100644 index 0000000..3ddf846 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.down.fizz @@ -0,0 +1,13 @@ +rename_table("selfservice_login_flows", "selfservice_login_requests") +rename_table("selfservice_login_flow_methods", "selfservice_login_request_methods") + +rename_table("selfservice_registration_flow_methods", "selfservice_registration_request_methods") +rename_table("selfservice_registration_flows", "selfservice_registration_requests") + +rename_table("selfservice_settings_flow_methods", "selfservice_settings_request_methods") +rename_table("selfservice_settings_flows", "selfservice_settings_requests") + +rename_table("selfservice_recovery_flow_methods", "selfservice_recovery_request_methods") +rename_table("selfservice_recovery_flows", "selfservice_recovery_requests") + +rename_table("selfservice_verification_flows", "selfservice_verification_requests") diff --git a/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz new file mode 100644 index 0000000..469afdd --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810161022_flow_rename.up.fizz @@ -0,0 +1,13 @@ +rename_table("selfservice_login_request_methods", "selfservice_login_flow_methods") +rename_table("selfservice_login_requests", "selfservice_login_flows") + +rename_table("selfservice_registration_request_methods", "selfservice_registration_flow_methods") +rename_table("selfservice_registration_requests", "selfservice_registration_flows") + +rename_table("selfservice_settings_request_methods", "selfservice_settings_flow_methods") +rename_table("selfservice_settings_requests", "selfservice_settings_flows") + +rename_table("selfservice_recovery_request_methods", "selfservice_recovery_flow_methods") +rename_table("selfservice_recovery_requests", "selfservice_recovery_flows") + +rename_table("selfservice_verification_requests", "selfservice_verification_flows") diff --git a/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz new file mode 100644 index 0000000..86a600c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.down.fizz @@ -0,0 +1,7 @@ +rename_column("selfservice_login_flow_methods", "selfservice_login_flow_id", "selfservice_login_request_id") + +rename_column("selfservice_registration_flow_methods", "selfservice_registration_flow_id", "selfservice_registration_request_id") + +rename_column("selfservice_settings_flow_methods", "selfservice_settings_flow_id", "selfservice_settings_request_id") + +rename_column("selfservice_recovery_flow_methods", "selfservice_recovery_flow_id", "selfservice_recovery_request_id") diff --git a/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz new file mode 100644 index 0000000..bc24ef3 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200810162450_flow_fields_rename.up.fizz @@ -0,0 +1,7 @@ +rename_column("selfservice_login_flow_methods", "selfservice_login_request_id", "selfservice_login_flow_id") + +rename_column("selfservice_registration_flow_methods", "selfservice_registration_request_id", "selfservice_registration_flow_id") + +rename_column("selfservice_recovery_flow_methods", "selfservice_recovery_request_id", "selfservice_recovery_flow_id") + +rename_column("selfservice_settings_flow_methods", "selfservice_settings_request_id", "selfservice_settings_flow_id") diff --git a/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz new file mode 100644 index 0000000..a25137a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.down.fizz @@ -0,0 +1 @@ +drop_column("sessions", "token") diff --git a/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz new file mode 100644 index 0000000..3a9141a --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812124254_add_session_token.up.fizz @@ -0,0 +1,7 @@ +sql("DELETE FROM sessions") + +add_column("sessions", "token", "string", {"size": 32, "null": true}) +change_column("sessions", "token", "string", {"size": 32, "null": false}) + +add_index("sessions", "token", {"unique": true, "name": "sessions_token_uq_idx"}) +add_index("sessions", "token", {"name": "sessions_token_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz new file mode 100644 index 0000000..23e604c --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.down.fizz @@ -0,0 +1 @@ +drop_column("sessions", "active") diff --git a/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz new file mode 100644 index 0000000..f858882 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200812160551_add_session_revoke.up.fizz @@ -0,0 +1 @@ +add_column("sessions", "active", "boolean", {"null": false, "default": false}) diff --git a/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz new file mode 100644 index 0000000..a05f0d5 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.down.fizz @@ -0,0 +1 @@ +rename_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "selfservice_recovery_request_id") diff --git a/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz new file mode 100644 index 0000000..8601646 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830121710_update_recovery_token.up.fizz @@ -0,0 +1 @@ +rename_column("identity_recovery_tokens", "selfservice_recovery_request_id", "selfservice_recovery_flow_id") diff --git a/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz new file mode 100644 index 0000000..2bb6500 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.down.fizz @@ -0,0 +1,16 @@ +{{ if or .IsPostgreSQL .IsMySQL .IsMariaDB }} + add_column("selfservice_verification_flows", "form", "json", { "null": true }) + sql("UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t);") + change_column("selfservice_verification_flows", "form", "json", { "null": false }) +{{ end }} + +{{ if .IsCockroach }} + add_column("selfservice_verification_flows", "form", "json", { "default": "{}" }) +{{ end }} + +drop_table("selfservice_verification_flow_methods") +drop_column("selfservice_verification_flows", "active_method") +drop_column("selfservice_verification_flows", "state") + +add_column("selfservice_verification_flows", "via", "string", {"size": 16, "default": "email"}) +add_column("selfservice_verification_flows", "success", "bool", {"default_raw": "FALSE"}) diff --git a/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz new file mode 100644 index 0000000..2819d93 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130642_add_verification_methods.up.fizz @@ -0,0 +1 @@ +add_column("selfservice_verification_flows", "state", "string", {"default": "show_form"}) diff --git a/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz new file mode 100644 index 0000000..376ec63 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130643_add_verification_methods.up.fizz @@ -0,0 +1 @@ +sql("UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE") diff --git a/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz new file mode 100644 index 0000000..250846b --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130644_add_verification_methods.up.fizz @@ -0,0 +1,8 @@ +create_table("selfservice_verification_flow_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_verification_flow_id", "uuid") + t.Column("config", "json") +} + +add_column("selfservice_verification_flows", "active_method", "string", {"size": 32, null: true}) diff --git a/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz new file mode 100644 index 0000000..acad208 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130645_add_verification_methods.up.fizz @@ -0,0 +1 @@ +sql("INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows;") diff --git a/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.down.fizz b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.down.fizz new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz new file mode 100644 index 0000000..ee0a577 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830130646_add_verification_methods.up.fizz @@ -0,0 +1,3 @@ +drop_column("selfservice_verification_flows", "form") +drop_column("selfservice_verification_flows", "via") +drop_column("selfservice_verification_flows", "success") diff --git a/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz new file mode 100644 index 0000000..beb5a42 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.down.fizz @@ -0,0 +1 @@ +drop_table("identity_verification_tokens") diff --git a/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz new file mode 100644 index 0000000..c6182dc --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830154602_add_verification_token.up.fizz @@ -0,0 +1,21 @@ +create_table("identity_verification_tokens") { + t.Column("id", "uuid", {primary: true}) + + t.Column("token", "string", {"size": 64}) + t.Column("used", "bool", {"default": false}) + t.Column("used_at", "timestamp", {"null": true}) + t.Column("expires_at", "timestamp") + t.Column("issued_at", "timestamp") + + t.Column("identity_verifiable_address_id", "uuid") + t.ForeignKey("identity_verifiable_address_id", {"identity_verifiable_addresses": ["id"]}, {"on_delete": "cascade"}) + + t.Column("selfservice_verification_flow_id", "uuid", {"null": true}) + t.ForeignKey("selfservice_verification_flow_id", {"selfservice_verification_flows": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_verification_tokens", ["token"], { "unique": true, "name": "identity_verification_tokens_token_uq_idx" }) +add_index("identity_verification_tokens", ["token"], { "name": "identity_verification_tokens_token_idx" }) + +add_index("identity_verification_tokens", ["identity_verifiable_address_id"], { "name": "identity_verification_tokens_verifiable_address_id_idx" }) +add_index("identity_verification_tokens", ["selfservice_verification_flow_id"], { "name": "identity_verification_tokens_verification_flow_id_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz new file mode 100644 index 0000000..ef694c9 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.down.fizz @@ -0,0 +1,4 @@ +sql("DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL") +change_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "uuid") +drop_column("identity_recovery_tokens", "expires_at") +drop_column("identity_recovery_tokens", "issued_at") diff --git a/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz new file mode 100644 index 0000000..aa8546e --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200830172221_recovery_token_expires.up.fizz @@ -0,0 +1,3 @@ +add_column("identity_recovery_tokens", "expires_at", "timestamp", { "default": "2000-01-01 00:00:00" }) +add_column("identity_recovery_tokens", "issued_at", "timestamp", { "default": "2000-01-01 00:00:00" }) +change_column("identity_recovery_tokens", "selfservice_recovery_flow_id", "uuid", {"null": true}) diff --git a/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz new file mode 100755 index 0000000..d0a9216 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.down.fizz @@ -0,0 +1,28 @@ +add_column("identity_verifiable_addresses", "code", "string", {"size": 32, "null": true}) +add_column("identity_verifiable_addresses", "expires_at", "timestamp", { "null": true }) + +{{ if .IsSQLite }} + sql("UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if or .IsMySQL .IsMariaDB }} + sql("UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if .IsPostgreSQL }} + sql("UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +{{ if .IsCockroach }} + sql("UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL") + sql("UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL") +{{ end }} + +change_column("identity_verifiable_addresses", "code", "string", {"size": 32}) +change_column("identity_verifiable_addresses", "expires_at", "timestamp", { "null": false }) + +add_index("identity_verifiable_addresses", ["code"], { "unique": true, "name": "identity_verifiable_addresses_code_uq_idx" }) +add_index("identity_verifiable_addresses", ["code"], { "name": "identity_verifiable_addresses_code_idx" }) diff --git a/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz new file mode 100755 index 0000000..4a1d779 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20200831110752_identity_verifiable_address_remove_code.up.fizz @@ -0,0 +1,5 @@ +drop_index("identity_verifiable_addresses", "identity_verifiable_addresses_code_uq_idx") +drop_index("identity_verifiable_addresses", "identity_verifiable_addresses_code_idx") + +drop_column("identity_verifiable_addresses", "code") +drop_column("identity_verifiable_addresses", "expires_at") diff --git a/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz new file mode 100644 index 0000000..ba68093 --- /dev/null +++ b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.down.fizz @@ -0,0 +1 @@ +sql("DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'") diff --git a/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz new file mode 100644 index 0000000..66512ad --- /dev/null +++ b/oryx/popx/stub/migrations/source/20201201161451_credential_types_values.up.fizz @@ -0,0 +1,3 @@ +sql("INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password')") +sql("INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc')") + diff --git a/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.down.sql b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql new file mode 100644 index 0000000..f73626d --- /dev/null +++ b/oryx/popx/stub/migrations/templating/0_sql_create_tablename_template.up.sql @@ -0,0 +1 @@ +CREATE TABLE {{ identifier .Parameters.tableName }} ( "id" UUID NOT NULL, PRIMARY KEY ("id")); diff --git a/oryx/popx/stub/migrations/testdata/20220513_testdata.invalid b/oryx/popx/stub/migrations/testdata/20220513_testdata.invalid new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata/20220513_testdata.sql b/oryx/popx/stub/migrations/testdata/20220513_testdata.sql new file mode 100644 index 0000000..6687fec --- /dev/null +++ b/oryx/popx/stub/migrations/testdata/20220513_testdata.sql @@ -0,0 +1 @@ +INSERT INTO testdata (Data) VALUES ('testdata'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/testdata/20220514_testdata.sql b/oryx/popx/stub/migrations/testdata/20220514_testdata.sql new file mode 100644 index 0000000..56d7b98 --- /dev/null +++ b/oryx/popx/stub/migrations/testdata/20220514_testdata.sql @@ -0,0 +1 @@ +-- empty migrations should not error \ No newline at end of file diff --git a/oryx/popx/stub/migrations/testdata/invalid b/oryx/popx/stub/migrations/testdata/invalid new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata/invalid_testdata.sql b/oryx/popx/stub/migrations/testdata/invalid_testdata.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.down.sql b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql new file mode 100644 index 0000000..59c85da --- /dev/null +++ b/oryx/popx/stub/migrations/testdata_migrations/20220513_create_table.up.sql @@ -0,0 +1,3 @@ +CREATE TABLE "testdata" ( + "data" character varying(255) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql new file mode 100644 index 0000000..ae0df01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.cockroach.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql new file mode 100644 index 0000000..ae2d9ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identities`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql new file mode 100644 index 0000000..f257ad0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.mysql.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE `identities` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`traits_schema_id` VARCHAR (2048) NOT NULL, +`traits` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql new file mode 100644 index 0000000..4fab90d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.postgres.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identities" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"traits_schema_id" VARCHAR (2048) NOT NULL, +"traits" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql new file mode 100644 index 0000000..bf3e56c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identities"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql new file mode 100644 index 0000000..7448d69 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000000_identities.sqlite3.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE "identities" ( +"id" TEXT PRIMARY KEY, +"traits_schema_id" TEXT NOT NULL, +"traits" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql new file mode 100644 index 0000000..a5245e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.cockroach.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql new file mode 100644 index 0000000..0440ced --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_types` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql new file mode 100644 index 0000000..52f95d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.mysql.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE `identity_credential_types` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`name` VARCHAR (32) NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql new file mode 100644 index 0000000..a5245e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.postgres.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE "identity_credential_types" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"name" VARCHAR (32) NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql new file mode 100644 index 0000000..f533e0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_types" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql new file mode 100644 index 0000000..a48493a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000001_identities.sqlite3.up.sql @@ -0,0 +1,4 @@ +CREATE TABLE "identity_credential_types" ( +"id" TEXT PRIMARY KEY, +"name" TEXT NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql new file mode 100644 index 0000000..6884f06 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credentials` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql new file mode 100644 index 0000000..4770736 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_types_name_idx` ON `identity_credential_types` (`name`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql new file mode 100644 index 0000000..6b34364 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credentials" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql new file mode 100644 index 0000000..a881431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000002_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_types_name_idx" ON "identity_credential_types" (name) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql new file mode 100644 index 0000000..25bb5e5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" json NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credentials_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +CONSTRAINT "identity_credentials_identity_credential_types_id_fk" FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql new file mode 100644 index 0000000..b96b95d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_credential_identifiers` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql new file mode 100644 index 0000000..ad1aa8a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `identity_credentials` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`config` JSON NOT NULL, +`identity_credential_type_id` char(36) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade, +FOREIGN KEY (`identity_credential_type_id`) REFERENCES `identity_credential_types` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql new file mode 100644 index 0000000..3bdf1c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "identity_credentials" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"config" jsonb NOT NULL, +"identity_credential_type_id" UUID NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade, +FOREIGN KEY ("identity_credential_type_id") REFERENCES "identity_credential_types" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql new file mode 100644 index 0000000..a56ae36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_credential_identifiers" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql new file mode 100644 index 0000000..190bcc0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000003_identities.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_credentials" ( +"id" TEXT PRIMARY KEY, +"config" TEXT NOT NULL, +"identity_credential_type_id" char(36) NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade, +FOREIGN KEY (identity_credential_type_id) REFERENCES identity_credential_types (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql new file mode 100644 index 0000000..5ebd7af --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_credential_identifiers_identity_credentials_id_fk" FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql new file mode 100644 index 0000000..a32c587 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `identity_credential_identifiers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identifier` VARCHAR (255) NOT NULL, +`identity_credential_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_credential_id`) REFERENCES `identity_credentials` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql new file mode 100644 index 0000000..2b20a63 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identifier" VARCHAR (255) NOT NULL, +"identity_credential_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_credential_id") REFERENCES "identity_credentials" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql new file mode 100644 index 0000000..e7ba7d7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000004_identities.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "identity_credential_identifiers" ( +"id" TEXT PRIMARY KEY, +"identifier" TEXT NOT NULL, +"identity_credential_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_credential_id) REFERENCES identity_credentials (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql new file mode 100644 index 0000000..759def9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_credential_identifiers_identifier_idx` ON `identity_credential_identifiers` (`identifier`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql new file mode 100644 index 0000000..fb24576 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000001000005_identities.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_credential_identifiers_identifier_idx" ON "identity_credential_identifiers" (identifier); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql new file mode 100644 index 0000000..2627dfe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql new file mode 100644 index 0000000..77db2f7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql new file mode 100644 index 0000000..5e7e552 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_login_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql new file mode 100644 index 0000000..2627dfe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql new file mode 100644 index 0000000..f5d0e0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql new file mode 100644 index 0000000..f7b5788 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000000_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql new file mode 100644 index 0000000..14c44bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_login_request_methods_selfservice_login_requests_id_fk" FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql new file mode 100644 index 0000000..0cc2d40 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql new file mode 100644 index 0000000..fbbcafb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_login_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_login_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_login_request_id`) REFERENCES `selfservice_login_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql new file mode 100644 index 0000000..fb69b6b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_login_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_login_request_id") REFERENCES "selfservice_login_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql new file mode 100644 index 0000000..9bb6db3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql new file mode 100644 index 0000000..48c9c7c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000001_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_login_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_login_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_login_request_id) REFERENCES selfservice_login_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql new file mode 100644 index 0000000..4c08df7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql new file mode 100644 index 0000000..eb5929d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_registration_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql new file mode 100644 index 0000000..596c6f4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `selfservice_registration_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`active_method` VARCHAR (32) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql new file mode 100644 index 0000000..4c08df7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"active_method" VARCHAR (32) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql new file mode 100644 index 0000000..d48f97c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql new file mode 100644 index 0000000..0342d26 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000002_requests.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql new file mode 100644 index 0000000..6adfd9f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_registration_request_methods_selfservice_registration_requests_id_fk" FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql new file mode 100644 index 0000000..481215f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql new file mode 100644 index 0000000..da52b0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_registration_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_registration_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_registration_request_id`) REFERENCES `selfservice_registration_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql new file mode 100644 index 0000000..d293ae9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_registration_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_registration_request_id") REFERENCES "selfservice_registration_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql new file mode 100644 index 0000000..8078deb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql new file mode 100644 index 0000000..68d1d38 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000003_requests.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_registration_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_registration_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_registration_request_id) REFERENCES selfservice_registration_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql new file mode 100644 index 0000000..d073080 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_profile_management_requests_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql new file mode 100644 index 0000000..2f7a60a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_login_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql new file mode 100644 index 0000000..1fc559a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_profile_management_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`update_successful` bool NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql new file mode 100644 index 0000000..7530465 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql new file mode 100644 index 0000000..f2c0d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql new file mode 100644 index 0000000..48fa320 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000002000004_requests.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_profile_management_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql new file mode 100644 index 0000000..9cf7fc2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "sessions_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql new file mode 100644 index 0000000..b37f476 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `sessions`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql new file mode 100644 index 0000000..ae325f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `sessions` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`authenticated_at` DATETIME NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql new file mode 100644 index 0000000..fab4323 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "sessions" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"authenticated_at" timestamp NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql new file mode 100644 index 0000000..d49b7ae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql new file mode 100644 index 0000000..c122664 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000003000000_sessions.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "sessions" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql new file mode 100644 index 0000000..a920e94 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" json NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql new file mode 100644 index 0000000..dcf8246 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_errors`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql new file mode 100644 index 0000000..b2afc3c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_errors` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`errors` JSON NOT NULL, +`seen_at` DATETIME NOT NULL, +`was_seen` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql new file mode 100644 index 0000000..e0a5c9e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_errors" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"errors" jsonb NOT NULL, +"seen_at" timestamp NOT NULL, +"was_seen" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..b6a3306 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..1eb73f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000004000000_errors.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_errors" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME NOT NULL, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql new file mode 100644 index 0000000..13fd1fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql new file mode 100644 index 0000000..0dc4431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000000_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql new file mode 100644 index 0000000..13fd1fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql new file mode 100644 index 0000000..0dc4431 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000005000001_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql new file mode 100644 index 0000000..1c69440 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `courier_messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql new file mode 100644 index 0000000..24e0ac9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `courier_messages` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`type` INTEGER NOT NULL, +`status` INTEGER NOT NULL, +`body` VARCHAR (255) NOT NULL, +`subject` VARCHAR (255) NOT NULL, +`recipient` VARCHAR (255) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql new file mode 100644 index 0000000..70af9f0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "courier_messages" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"type" int NOT NULL, +"status" int NOT NULL, +"body" VARCHAR (255) NOT NULL, +"subject" VARCHAR (255) NOT NULL, +"recipient" VARCHAR (255) NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql new file mode 100644 index 0000000..0d9747b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql new file mode 100644 index 0000000..e718e31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000006000000_courier.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "courier_messages" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql new file mode 100644 index 0000000..9fbb33c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` DROP COLUMN `csrf_token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql new file mode 100644 index 0000000..f54bdc2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` ADD COLUMN `csrf_token` VARCHAR (255) NOT NULL DEFAULT ""; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql new file mode 100644 index 0000000..6f93d74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "csrf_token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql new file mode 100644 index 0000000..4e04c0f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" VARCHAR (255) NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..f55e6a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000000_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "csrf_token" TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..c1af035 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..1a2d145 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at) SELECT id, errors, seen_at, was_seen, created_at, updated_at FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..11afafe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.down.sql @@ -0,0 +1,8 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000007000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..83c7bd3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verifiable_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..10d3639 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verifiable_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..207fc03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verifiable_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`code` VARCHAR (32) NOT NULL, +`status` VARCHAR (16) NOT NULL, +`via` VARCHAR (16) NOT NULL, +`verified` bool NOT NULL, +`value` VARCHAR (400) NOT NULL, +`verified_at` DATETIME, +`expires_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..300e534 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"code" VARCHAR (32) NOT NULL, +"status" VARCHAR (16) NOT NULL, +"via" VARCHAR (16) NOT NULL, +"verified" bool NOT NULL, +"value" VARCHAR (400) NOT NULL, +"verified_at" timestamp, +"expires_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..2c3a752 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..e920a7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000000_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verifiable_addresses" ( +"id" TEXT PRIMARY KEY, +"code" TEXT NOT NULL, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..da1b36a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3f787b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..79aff96 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000001_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3df0610 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..cad7d49 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000002_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..3deaf4e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_status_via_uq_idx` ON `identity_verifiable_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..703f37c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000003_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..5380dd4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_status_via_idx` ON `identity_verifiable_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..918ff3f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000004_selfservice_verification.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "identity_verifiable_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql new file mode 100644 index 0000000..e843d51 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" json NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql new file mode 100644 index 0000000..a3dcda2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `selfservice_verification_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`form` JSON NOT NULL, +`via` VARCHAR (16) NOT NULL, +`csrf_token` VARCHAR (255) NOT NULL, +`success` bool NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql new file mode 100644 index 0000000..86d22fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"form" jsonb NOT NULL, +"via" VARCHAR (16) NOT NULL, +"csrf_token" VARCHAR (255) NOT NULL, +"success" bool NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql new file mode 100644 index 0000000..c4c063f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000008000005_selfservice_verification.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "selfservice_verification_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"form" TEXT NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql new file mode 100644 index 0000000..4520233 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql new file mode 100644 index 0000000..4520233 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000009000001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql new file mode 100644 index 0000000..9687536 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql new file mode 100644 index 0000000..6e09789 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_errors` MODIFY `seen_at` DATETIME; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql new file mode 100644 index 0000000..57ee024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ALTER COLUMN "seen_at" TYPE timestamp, ALTER COLUMN "seen_at" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql new file mode 100644 index 0000000..1ab82fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000000_errors.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql new file mode 100644 index 0000000..3fd6e5e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql new file mode 100644 index 0000000..670dfb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql new file mode 100644 index 0000000..ffaad71 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql new file mode 100644 index 0000000..f8924c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000001_errors.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql new file mode 100644 index 0000000..670dfb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" ADD COLUMN "seen_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql new file mode 100644 index 0000000..3fd6e5e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "selfservice_errors" SET "seen_at" = "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql new file mode 100644 index 0000000..f8924c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_errors_tmp" (id, errors, seen_at, was_seen, created_at, updated_at, csrf_token) SELECT id, errors, seen_at, was_seen, created_at, updated_at, csrf_token FROM "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql new file mode 100644 index 0000000..ffaad71 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000002_errors.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_errors" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql new file mode 100644 index 0000000..9687536 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" RENAME COLUMN "seen_at" TO "_seen_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql new file mode 100644 index 0000000..ebf18f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_errors" DROP COLUMN "_seen_at_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql new file mode 100644 index 0000000..1ab82fb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.down.sql @@ -0,0 +1,9 @@ +CREATE TABLE "_selfservice_errors_tmp" ( +"id" TEXT PRIMARY KEY, +"errors" TEXT NOT NULL, +"seen_at" DATETIME, +"was_seen" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL DEFAULT '' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql new file mode 100644 index 0000000..95b13b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000003_errors.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_errors_tmp" RENAME TO "selfservice_errors"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql new file mode 100644 index 0000000..a0b1973 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_errors SET seen_at = '1980-01-01 00:00:00' WHERE seen_at = NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000010000004_errors.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..046714a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" RENAME COLUMN "body" TO "_body_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql new file mode 100644 index 0000000..2823561 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `courier_messages` MODIFY `body` text NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql new file mode 100644 index 0000000..55a3ecf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" TYPE text, ALTER COLUMN "body" SET NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..fe38552 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000000_courier_body_type.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_courier_messages_tmp" ( +"id" TEXT PRIMARY KEY, +"type" INTEGER NOT NULL, +"status" INTEGER NOT NULL, +"body" TEXT NOT NULL, +"subject" TEXT NOT NULL, +"recipient" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..88ff829 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ADD COLUMN "body" text \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..ca6ae21 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000001_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_courier_messages_tmp" (id, type, status, body, subject, recipient, created_at, updated_at) SELECT id, type, status, body, subject, recipient, created_at, updated_at FROM "courier_messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..0c6a568 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "courier_messages" SET "body" = "_body_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..0623a36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000002_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "courier_messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..fe0b4ca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" ALTER COLUMN "body" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql new file mode 100644 index 0000000..a91ff04 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000003_courier_body_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_courier_messages_tmp" RENAME TO "courier_messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql new file mode 100644 index 0000000..228eae4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000011000004_courier_body_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "courier_messages" DROP COLUMN "_body_tmp"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql new file mode 100644 index 0000000..acdb077 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `forced`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql new file mode 100644 index 0000000..d1a0dce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `forced` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql new file mode 100644 index 0000000..8dbb746 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "forced"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..fd575c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..b84202f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000000_login_request_forced.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "forced" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000001_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..7fd3f62 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000002_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql new file mode 100644 index 0000000..9d051c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20191100000012000003_login_request_forced.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..04ae974 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..0e22af6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `active_method`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..9cc49fa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_profile_management_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_profile_management_request_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..3b57e83 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "active_method"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..bc0b322 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_profile_management_request_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..566fcf6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000000_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_profile_management_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_profile_management_request_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..b10ec65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..9867e64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_profile_management_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..e392ad8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `active_method` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..b10ec65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f61bf8d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..5366d7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000001_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "active_method" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..edfcfd4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" json NOT NULL DEFAULT '{}' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..e4dbeac --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` MODIFY `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..f7e14b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..f071ec2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, update_successful FROM "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..50a0e95 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000002_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_profile_management_request_methods (id, method, selfservice_profile_management_request_id, config) SELECT id, 'traits', id, form FROM selfservice_profile_management_requests \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..7793d13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..adbcdb4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` DROP COLUMN `form`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..7793d13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_profile_management_requests SET form=(SELECT * FROM (SELECT m.config FROM selfservice_profile_management_requests AS r INNER JOIN selfservice_profile_management_request_methods AS m ON r.id=m.selfservice_profile_management_request_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..ea65344 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" DROP COLUMN "form"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..669e51a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..c82eda8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000003_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_profile_management_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"update_successful" bool NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql new file mode 100644 index 0000000..f4861a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` ADD COLUMN `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql new file mode 100644 index 0000000..0c541b8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" ADD COLUMN "form" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e263b28 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..ecd6b61 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000004_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_profile_management_requests_tmp" (id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method) SELECT id, request_url, issued_at, expires_at, update_successful, identity_id, created_at, updated_at, active_method FROM "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..f61bf8d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000005_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_profile_management_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql new file mode 100644 index 0000000..62711a0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200317160354000006_create_profile_request_forms.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_profile_management_requests_tmp" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql new file mode 100644 index 0000000..cf9678d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.cockroach.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" json, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "continuity_containers_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql new file mode 100644 index 0000000..17396f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `continuity_containers`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql new file mode 100644 index 0000000..42b5531 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.mysql.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE `continuity_containers` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`identity_id` char(36), +`name` VARCHAR (255) NOT NULL, +`payload` JSON, +`expires_at` DATETIME NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql new file mode 100644 index 0000000..ab8cfd5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.postgres.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "continuity_containers" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"identity_id" UUID, +"name" VARCHAR (255) NOT NULL, +"payload" jsonb, +"expires_at" timestamp NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql new file mode 100644 index 0000000..3aef425 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "continuity_containers"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql new file mode 100644 index 0000000..b0e0182 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200401183443000000_continuity_containers.sqlite3.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "continuity_containers" ( +"id" TEXT PRIMARY KEY, +"identity_id" char(36), +"name" TEXT NOT NULL, +"payload" TEXT, +"expires_at" DATETIME NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..c0c0ace --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_profile_management_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..81040d7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` CHANGE `selfservice_profile_management_request_id` `selfservice_settings_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..52a8b09 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_profile_management_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..ca1a50c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000000_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME COLUMN "selfservice_profile_management_request_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..39fefdb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_profile_management_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..cf51250 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_request_methods` RENAME TO `selfservice_settings_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..1873dde --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_profile_management_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..2c4303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000001_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_request_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql new file mode 100644 index 0000000..978cf22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` CHANGE `selfservice_settings_request_id` `selfservice_profile_management_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql new file mode 100644 index 0000000..29362dd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_profile_management_requests` RENAME TO `selfservice_settings_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql new file mode 100644 index 0000000..26a15a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_profile_management_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql new file mode 100644 index 0000000..e62d4ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200402142539000002_rename_profile_flows.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_profile_management_requests" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..0d18953 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_addresses_identities_id_fk" FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..d79504e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_addresses`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..432c984 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `identity_recovery_addresses` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`via` VARCHAR (16) NOT NULL, +`value` VARCHAR (400) NOT NULL, +`identity_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..b6ba272 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"via" VARCHAR (16) NOT NULL, +"value" VARCHAR (400) NOT NULL, +"identity_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..51596f1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..7663c75 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000000_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "identity_recovery_addresses" ( +"id" TEXT PRIMARY KEY, +"via" TEXT NOT NULL, +"value" TEXT NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..9372cdd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..665e86f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_status_via_uq_idx` ON `identity_recovery_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..3e68e81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..02e829b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000001_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_status_via_uq_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..9843693 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_recovery_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..9235aca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_status_via_idx` ON `identity_recovery_addresses` (`via`, `value`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..f9da8be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..1c34d39 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000002_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_status_via_idx" ON "identity_recovery_addresses" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..91fe16e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" json, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_requests_identities_id_fk" FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..34a95c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_recovery_tokens` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..ecd5ac3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `selfservice_recovery_requests` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`request_url` VARCHAR (2048) NOT NULL, +`issued_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +`expires_at` DATETIME NOT NULL, +`messages` JSON, +`active_method` VARCHAR (32), +`csrf_token` VARCHAR (255) NOT NULL, +`state` VARCHAR (32) NOT NULL, +`recovered_identity_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`recovered_identity_id`) REFERENCES `identities` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..ff29b1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"request_url" VARCHAR (2048) NOT NULL, +"issued_at" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" timestamp NOT NULL, +"messages" jsonb, +"active_method" VARCHAR (32), +"csrf_token" VARCHAR (255) NOT NULL, +"state" VARCHAR (32) NOT NULL, +"recovered_identity_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("recovered_identity_id") REFERENCES "identities" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..4f5b77b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000003_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "selfservice_recovery_requests" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..ae150ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" json NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "selfservice_recovery_request_methods_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..0b84cbc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE `selfservice_recovery_request_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`config` JSON NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..bb577b3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,10 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"config" jsonb NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..832fb04 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000004_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_recovery_request_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"config" TEXT NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..8ce83be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_recovery_tokens_identity_recovery_addresses_id_fk" FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..346f257 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.mysql.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE `identity_recovery_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`identity_recovery_address_id` char(36) NOT NULL, +`selfservice_recovery_request_id` char(36) NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_recovery_address_id`) REFERENCES `identity_recovery_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_recovery_request_id`) REFERENCES `selfservice_recovery_requests` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..d00c2ba --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.postgres.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"identity_recovery_address_id" UUID NOT NULL, +"selfservice_recovery_request_id" UUID NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_recovery_address_id") REFERENCES "identity_recovery_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_recovery_request_id") REFERENCES "selfservice_recovery_requests" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..0b57fdc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000005_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "identity_recovery_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_request_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_recovery_request_id) REFERENCES selfservice_recovery_requests (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..a04f5b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_recovery_addresses_code_uq_idx` ON `identity_recovery_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..b8444be --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000006_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "identity_recovery_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..0fb0af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_recovery_addresses_code_idx` ON `identity_recovery_tokens` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql new file mode 100644 index 0000000..38bc780 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101057000007_create_recovery_addresses.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "identity_recovery_tokens" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e620ccb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..893940c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000000_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql new file mode 100644 index 0000000..e620ccb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql new file mode 100644 index 0000000..893940c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200519101058000001_create_recovery_addresses.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_recovery_tokens MODIFY COLUMN token VARCHAR(64) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql new file mode 100644 index 0000000..d1e8b79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql new file mode 100644 index 0000000..d80e1ca --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql new file mode 100644 index 0000000..2c843fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql new file mode 100644 index 0000000..a9ca7f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql new file mode 100644 index 0000000..e5b5661 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..587ca18 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000000_create_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000001_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..02da0e4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, update_successful FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000002_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql new file mode 100644 index 0000000..ba66094 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"update_successful" bool NOT NULL DEFAULT 'false', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200601101000000003_create_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql new file mode 100644 index 0000000..7457218 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000000_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql new file mode 100644 index 0000000..4c7ee79 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(255) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql new file mode 100644 index 0000000..7457218 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200601101001000001_verification.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_verifiable_addresses MODIFY COLUMN code VARCHAR(32) BINARY \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql new file mode 100644 index 0000000..49925a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" json \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql new file mode 100644 index 0000000..81f1b3b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `messages`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql new file mode 100644 index 0000000..6ebd845 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `messages` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql new file mode 100644 index 0000000..012ee65 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "messages"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql new file mode 100644 index 0000000..afd33ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql new file mode 100644 index 0000000..0ff2399 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql new file mode 100644 index 0000000..f012c22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000000_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "messages" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql new file mode 100644 index 0000000..8d44e29 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql new file mode 100644 index 0000000..d70352b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" json \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql new file mode 100644 index 0000000..06ca9ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `messages` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql new file mode 100644 index 0000000..4e73ab7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `messages` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql new file mode 100644 index 0000000..8d44e29 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql new file mode 100644 index 0000000..e5a4417 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql new file mode 100644 index 0000000..52b78bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql new file mode 100644 index 0000000..3354242 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000001_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "messages" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql new file mode 100644 index 0000000..352e5f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql new file mode 100644 index 0000000..3d9176a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" json; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql new file mode 100644 index 0000000..5363bf6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `messages` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql new file mode 100644 index 0000000..d67d0f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `messages` JSON; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql new file mode 100644 index 0000000..352e5f9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "messages" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql new file mode 100644 index 0000000..41236ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" jsonb; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql new file mode 100644 index 0000000..87ef40b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at FROM "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql new file mode 100644 index 0000000..a99388b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000002_messages.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "messages" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql new file mode 100644 index 0000000..42808eb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000003_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql new file mode 100644 index 0000000..d5fb51f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000004_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000005_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql new file mode 100644 index 0000000..c1f05c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000006_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql new file mode 100644 index 0000000..b458c0b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000007_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql new file mode 100644 index 0000000..ee747b8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000008_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql new file mode 100644 index 0000000..96225c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000009_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql new file mode 100644 index 0000000..2203ba5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, via, success FROM "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000010_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql new file mode 100644 index 0000000..d9ad85e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200605111551000011_messages.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql new file mode 100644 index 0000000..c387b8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql new file mode 100644 index 0000000..8455290 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `update_successful` bool NOT NULL DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql new file mode 100644 index 0000000..fcf5408 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql new file mode 100644 index 0000000..c387b8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql new file mode 100644 index 0000000..51c468b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "update_successful" bool NOT NULL DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql new file mode 100644 index 0000000..97a3f7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000000_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql new file mode 100644 index 0000000..1300963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql new file mode 100644 index 0000000..bec242d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `state` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql new file mode 100644 index 0000000..b922439 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `update_successful`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql new file mode 100644 index 0000000..1300963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql new file mode 100644 index 0000000..601e8d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "update_successful"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql new file mode 100644 index 0000000..3e88236 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql new file mode 100644 index 0000000..b246a8b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000001_settings.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql new file mode 100644 index 0000000..cb697e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000002_settings.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql new file mode 100644 index 0000000..56700ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000003_settings.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql new file mode 100644 index 0000000..37abfb0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql new file mode 100644 index 0000000..002764d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200607165100000004_settings.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql new file mode 100644 index 0000000..7e3303f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `schema_id` `traits_schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql new file mode 100644 index 0000000..92a92fa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identities` CHANGE `traits_schema_id` `schema_id` varchar(2048) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql new file mode 100644 index 0000000..d2dee7d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "schema_id" TO "traits_schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql new file mode 100644 index 0000000..ce7cd59 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200705105359000000_rename_identities_schema.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identities" RENAME COLUMN "traits_schema_id" TO "schema_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql new file mode 100644 index 0000000..8e01074 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql new file mode 100644 index 0000000..5fae17d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` DROP COLUMN `type`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql new file mode 100644 index 0000000..b2a3fd7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql new file mode 100644 index 0000000..3ca09c1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" DROP COLUMN "type"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql new file mode 100644 index 0000000..8e01074 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..f8c6563 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_requests_tmp" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e98be86 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000000_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql new file mode 100644 index 0000000..e843d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql new file mode 100644 index 0000000..18cff22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql new file mode 100644 index 0000000..18aefc6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql new file mode 100644 index 0000000..272b917 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql new file mode 100644 index 0000000..e843d1e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql new file mode 100644 index 0000000..18cff22 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..96225c4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..5a7f522 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000001_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql new file mode 100644 index 0000000..b178dfb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql new file mode 100644 index 0000000..b8909ab --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql new file mode 100644 index 0000000..15060f4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql new file mode 100644 index 0000000..13b7c0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql new file mode 100644 index 0000000..b178dfb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql new file mode 100644 index 0000000..b8909ab --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..da7567f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_requests_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, via, success FROM "selfservice_verification_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..2e42a66 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000002_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql new file mode 100644 index 0000000..57ed47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql new file mode 100644 index 0000000..5f675f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql new file mode 100644 index 0000000..c97f5c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql new file mode 100644 index 0000000..57e979d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql new file mode 100644 index 0000000..57ed47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql new file mode 100644 index 0000000..5f675f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ae4ffd5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_verification_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"via" TEXT NOT NULL DEFAULT 'email', +"success" bool NOT NULL DEFAULT 'FALSE' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..8e5b854 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000003_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql new file mode 100644 index 0000000..f0fd55f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql new file mode 100644 index 0000000..c6195cd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` DROP COLUMN `type` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql new file mode 100644 index 0000000..c1282d8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` ADD COLUMN `type` VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql new file mode 100644 index 0000000..f0fd55f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" DROP COLUMN "type" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql new file mode 100644 index 0000000..ac97bcf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" VARCHAR (16) NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..e5c12a7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_recovery_requests_tmp" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..3bf96ef --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000004_flow_type.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" ADD COLUMN "type" TEXT NOT NULL DEFAULT 'browser'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..90bee92 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000005_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ed36b70 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_recovery_requests_tmp" (id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at) SELECT id, request_url, issued_at, expires_at, messages, active_method, csrf_token, state, recovered_identity_id, created_at, updated_at FROM "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000006_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..ede1b13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_recovery_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"messages" TEXT, +"active_method" TEXT, +"csrf_token" TEXT NOT NULL, +"state" TEXT NOT NULL, +"recovered_identity_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (recovered_identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000007_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..3e88236 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_settings_requests_tmp" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000008_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..d93ea64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000009_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..cb697e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_settings_requests_tmp" (id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state) SELECT id, request_url, issued_at, expires_at, identity_id, created_at, updated_at, active_method, messages, state FROM "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000010_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..b246a8b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_settings_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"active_method" TEXT, +"messages" TEXT, +"state" TEXT NOT NULL DEFAULT 'show_form', +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000011_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..6ff6407 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_registration_requests_tmp" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000012_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..52b78bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000013_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..0aa91e5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_registration_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, messages FROM "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000014_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..7a7e430 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_registration_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000015_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..d5fb51f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_login_requests_tmp" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000016_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..47b51d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000017_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..7ea2d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_login_requests_tmp" (id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages) SELECT id, request_url, issued_at, expires_at, active_method, csrf_token, created_at, updated_at, forced, messages FROM "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000018_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql new file mode 100644 index 0000000..2de4018 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_login_requests_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"active_method" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"forced" bool NOT NULL DEFAULT 'false', +"messages" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810141652000019_flow_type.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql new file mode 100644 index 0000000..dd9aedc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` RENAME TO `selfservice_verification_requests`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql new file mode 100644 index 0000000..137df74 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_request_methods` RENAME TO `selfservice_login_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..c021824 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" RENAME TO "selfservice_verification_requests"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..bb17cb8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000000_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_request_methods" RENAME TO "selfservice_login_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql new file mode 100644 index 0000000..694cabe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flows` RENAME TO `selfservice_recovery_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql new file mode 100644 index 0000000..ce602c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_requests` RENAME TO `selfservice_login_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..ef8a5e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flows" RENAME TO "selfservice_recovery_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..d308739 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000001_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_requests" RENAME TO "selfservice_login_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql new file mode 100644 index 0000000..322ca14 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` RENAME TO `selfservice_recovery_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql new file mode 100644 index 0000000..970fd35 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_request_methods` RENAME TO `selfservice_registration_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..1d4e3e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME TO "selfservice_recovery_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..c4e26c5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000002_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_request_methods" RENAME TO "selfservice_registration_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql new file mode 100644 index 0000000..763c5b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flows` RENAME TO `selfservice_settings_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql new file mode 100644 index 0000000..8a04ed3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_requests` RENAME TO `selfservice_registration_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..be2f9fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flows" RENAME TO "selfservice_settings_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..282f65f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000003_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_requests" RENAME TO "selfservice_registration_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql new file mode 100644 index 0000000..ebb598e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` RENAME TO `selfservice_settings_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql new file mode 100644 index 0000000..2215df5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_request_methods` RENAME TO `selfservice_settings_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..446ce6f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME TO "selfservice_settings_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..6c1dba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000004_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_request_methods" RENAME TO "selfservice_settings_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql new file mode 100644 index 0000000..54935f6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flows` RENAME TO `selfservice_registration_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql new file mode 100644 index 0000000..bf9ed12 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_requests` RENAME TO `selfservice_settings_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..dfc5acb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flows" RENAME TO "selfservice_registration_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..c0d5ed2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000005_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_requests" RENAME TO "selfservice_settings_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql new file mode 100644 index 0000000..932e415 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` RENAME TO `selfservice_registration_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql new file mode 100644 index 0000000..264b7e8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_request_methods` RENAME TO `selfservice_recovery_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..b91fa01 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME TO "selfservice_registration_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..bf5aed7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000006_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_request_methods" RENAME TO "selfservice_recovery_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql new file mode 100644 index 0000000..630e470 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` RENAME TO `selfservice_login_request_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql new file mode 100644 index 0000000..406ca1d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_requests` RENAME TO `selfservice_recovery_flows` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..0d0dd5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME TO "selfservice_login_request_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..01ebd67 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000007_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_requests" RENAME TO "selfservice_recovery_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql new file mode 100644 index 0000000..0175446 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flows` RENAME TO `selfservice_login_requests` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql new file mode 100644 index 0000000..dc3fdb9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_requests` RENAME TO `selfservice_verification_flows`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql new file mode 100644 index 0000000..9a761d9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flows" RENAME TO "selfservice_login_requests" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql new file mode 100644 index 0000000..8a8a244 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810161022000008_flow_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_requests" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..dcc2ab2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..3455d87 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_request_id` `selfservice_login_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..77c2a0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..44454b9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000000_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_request_id" TO "selfservice_login_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..e1cf4cc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_flow_id` `selfservice_settings_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..712063c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_request_id` `selfservice_registration_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..47d4a55 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_flow_id" TO "selfservice_settings_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..f57de36 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000001_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_request_id" TO "selfservice_registration_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..18f0622 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_registration_flow_methods` CHANGE `selfservice_registration_flow_id` `selfservice_registration_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..3084591 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_recovery_flow_methods` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..9475fea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_registration_flow_methods" RENAME COLUMN "selfservice_registration_flow_id" TO "selfservice_registration_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..9725045 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000002_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_recovery_flow_methods" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql new file mode 100644 index 0000000..b630499 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_login_flow_methods` CHANGE `selfservice_login_flow_id` `selfservice_login_request_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql new file mode 100644 index 0000000..78ea09e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_settings_flow_methods` CHANGE `selfservice_settings_request_id` `selfservice_settings_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql new file mode 100644 index 0000000..e9fc8f8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_login_flow_methods" RENAME COLUMN "selfservice_login_flow_id" TO "selfservice_login_request_id" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql new file mode 100644 index 0000000..8584637 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200810162450000003_flow_fields_rename.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_settings_flow_methods" RENAME COLUMN "selfservice_settings_request_id" TO "selfservice_settings_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql new file mode 100644 index 0000000..3ee676e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `token`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql new file mode 100644 index 0000000..9cab681 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "token"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..377599c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000000_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DELETE FROM sessions \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql new file mode 100644 index 0000000..9581d45 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `token` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..4822fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..2472546 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000001_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..bdf13df --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" RENAME COLUMN "token" TO "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql new file mode 100644 index 0000000..f8ee5fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` MODIFY `token` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql new file mode 100644 index 0000000..86c4207 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ALTER COLUMN "token" TYPE VARCHAR (32), ALTER COLUMN "token" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..c633c75 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..84df028 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000002_add_session_token.sqlite3.up.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..572e06e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "token" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql new file mode 100644 index 0000000..c8e8a19 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `sessions_token_uq_idx` ON `sessions` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..0fef07d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.down.sql @@ -0,0 +1,10 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..38d1dfa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000003_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..516cec3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "sessions" SET "token" = "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql new file mode 100644 index 0000000..b4c20a1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `sessions_token_idx` ON `sessions` (`token`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..9db98fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..9d2a3fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000004_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..14772cc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "_token_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..b83a7e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..961bbb6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000005_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..efe335d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000006_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "sessions" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql new file mode 100644 index 0000000..cf8e9db --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812124254000007_add_session_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "sessions" (token); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql new file mode 100644 index 0000000..fd675bf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` DROP COLUMN `active`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql new file mode 100644 index 0000000..80f88e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `sessions` ADD COLUMN `active` boolean DEFAULT false; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql new file mode 100644 index 0000000..4e81ca5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "active"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql new file mode 100644 index 0000000..d0f2384 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" boolean DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..4fccd03 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_sessions_tmp" RENAME TO "sessions"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..7730257 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000000_add_session_revoke.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "active" NUMERIC DEFAULT 'false'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..4822fe6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000001_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..38d1dfa --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_sessions_tmp" (id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token) SELECT id, issued_at, expires_at, authenticated_at, identity_id, created_at, updated_at, token FROM "sessions" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000002_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..1905c70 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "sessions_token_uq_idx" ON "_sessions_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000003_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..37fa47b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "sessions_token_idx" ON "_sessions_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000004_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..84df028 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_sessions_tmp" ( +"id" TEXT PRIMARY KEY, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"authenticated_at" DATETIME NOT NULL, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"token" TEXT, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000005_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..b83a7e2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000006_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql new file mode 100644 index 0000000..9db98fd --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "sessions_token_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200812160551000007_add_session_revoke.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql new file mode 100644 index 0000000..b1096fe --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_flow_id` `selfservice_recovery_request_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql new file mode 100644 index 0000000..26017ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` CHANGE `selfservice_recovery_request_id` `selfservice_recovery_flow_id` char(36) NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql new file mode 100644 index 0000000..5dad6b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "selfservice_recovery_request_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql new file mode 100644 index 0000000..3f0a2da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830121710000000_update_recovery_token.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_request_id" TO "selfservice_recovery_flow_id"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..970590a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `success` bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ee5b748 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `state` VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..5792ef9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" VARCHAR (255) NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..42b1738 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "success" bool NOT NULL DEFAULT FALSE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..af3d919 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "state" TEXT NOT NULL DEFAULT 'show_form'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..4ea5af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..76acb84 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `via` VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..4ea5af8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" VARCHAR (16) NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e340455 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "via" TEXT NOT NULL DEFAULT 'email' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..9cb268b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..cb69380 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `state` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..9cb268b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "state" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..554453e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..18701c6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `active_method` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..554453e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "active_method" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..0bcec61 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `selfservice_verification_flow_methods` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..cb5cafc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..6636766 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" json NOT NULL DEFAULT '{}' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..5a06270 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` MODIFY `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..649bea3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ALTER COLUMN "form" TYPE jsonb, ALTER COLUMN "form" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..d401b5b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,11 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..9a097ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..9a097ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET form=(SELECT * FROM (SELECT m.config FROM selfservice_verification_flows AS r INNER JOIN selfservice_verification_flow_methods AS m ON r.id=m.selfservice_verification_flow_id) as t) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..a27a777 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `form` JSON \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..8ac44ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "form" jsonb \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..99f1a46 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..9f9be3b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form' +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e66885f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "selfservice_verification_flow_methods" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130642000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ea4615e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130643000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +UPDATE selfservice_verification_flows SET state='passed_challenge' WHERE success IS TRUE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..a1a5596 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" json NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..5a36bae --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.mysql.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE `selfservice_verification_flow_methods` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`method` VARCHAR (32) NOT NULL, +`selfservice_verification_flow_id` char(36) NOT NULL, +`config` JSON NOT NULL, +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..c4234d0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.postgres.up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"method" VARCHAR (32) NOT NULL, +"selfservice_verification_flow_id" UUID NOT NULL, +"config" jsonb NOT NULL, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..ca15b0a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,8 @@ +CREATE TABLE "selfservice_verification_flow_methods" ( +"id" TEXT PRIMARY KEY, +"method" TEXT NOT NULL, +"selfservice_verification_flow_id" char(36) NOT NULL, +"config" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..621021c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` ADD COLUMN `active_method` VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..85087a8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" VARCHAR (32); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..2568649 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130644000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" ADD COLUMN "active_method" TEXT; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..6b5e1fc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130645000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO selfservice_verification_flow_methods (id, method, selfservice_verification_flow_id, config, created_at, updated_at) SELECT id, 'link', id, form, created_at, updated_at FROM selfservice_verification_flows; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..690a58c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..6cb7200 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `form` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..690a58c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "form" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..90f5503 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000000_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"via" TEXT NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..00117a6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..e5375ff --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `via` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..00117a6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "via" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..02c3c0c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000001_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, via, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql new file mode 100644 index 0000000..ae68024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `selfservice_verification_flows` DROP COLUMN `success`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql new file mode 100644 index 0000000..b775d31 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "selfservice_verification_flows" DROP COLUMN "success"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000002_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000003_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..5289902 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000004_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"success" bool NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..7fe65c3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000005_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, success, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000006_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1444ec9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000007_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..e7c586c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000008_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_selfservice_verification_flows_tmp" ( +"id" TEXT PRIMARY KEY, +"request_url" TEXT NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"expires_at" DATETIME NOT NULL, +"csrf_token" TEXT NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"messages" TEXT, +"type" TEXT NOT NULL DEFAULT 'browser', +"state" TEXT NOT NULL DEFAULT 'show_form', +"active_method" TEXT +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..1e7cac1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000009_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_selfservice_verification_flows_tmp" (id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method) SELECT id, request_url, issued_at, expires_at, csrf_token, created_at, updated_at, messages, type, state, active_method FROM "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..fa7f929 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000010_add_verification_methods.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "selfservice_verification_flows" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql new file mode 100644 index 0000000..030cc33 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830130646000011_add_verification_methods.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_selfservice_verification_flows_tmp" RENAME TO "selfservice_verification_flows"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..4ea42da --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.cockroach.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +CONSTRAINT "identity_verification_tokens_identity_verifiable_addresses_id_fk" FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +CONSTRAINT "identity_verification_tokens_selfservice_verification_flows_id_fk" FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..5696963 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.down.sql @@ -0,0 +1 @@ +DROP TABLE `identity_verification_tokens`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..b582095 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.mysql.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE `identity_verification_tokens` ( +`id` char(36) NOT NULL, +PRIMARY KEY(`id`), +`token` VARCHAR (64) NOT NULL, +`used` bool NOT NULL DEFAULT false, +`used_at` DATETIME, +`expires_at` DATETIME NOT NULL, +`issued_at` DATETIME NOT NULL, +`identity_verifiable_address_id` char(36) NOT NULL, +`selfservice_verification_flow_id` char(36), +`created_at` DATETIME NOT NULL, +`updated_at` DATETIME NOT NULL, +FOREIGN KEY (`identity_verifiable_address_id`) REFERENCES `identity_verifiable_addresses` (`id`) ON DELETE cascade, +FOREIGN KEY (`selfservice_verification_flow_id`) REFERENCES `selfservice_verification_flows` (`id`) ON DELETE cascade +) ENGINE=InnoDB \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..4a50778 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.postgres.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" UUID NOT NULL, +PRIMARY KEY("id"), +"token" VARCHAR (64) NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" timestamp, +"expires_at" timestamp NOT NULL, +"issued_at" timestamp NOT NULL, +"identity_verifiable_address_id" UUID NOT NULL, +"selfservice_verification_flow_id" UUID, +"created_at" timestamp NOT NULL, +"updated_at" timestamp NOT NULL, +FOREIGN KEY ("identity_verifiable_address_id") REFERENCES "identity_verifiable_addresses" ("id") ON DELETE cascade, +FOREIGN KEY ("selfservice_verification_flow_id") REFERENCES "selfservice_verification_flows" ("id") ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..8b45572 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verification_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..cab945e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000000_add_verification_token.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "identity_verification_tokens" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"expires_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL, +"identity_verifiable_address_id" char(36) NOT NULL, +"selfservice_verification_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_verifiable_address_id) REFERENCES identity_verifiable_addresses (id) ON DELETE cascade, +FOREIGN KEY (selfservice_verification_flow_id) REFERENCES selfservice_verification_flows (id) ON DELETE cascade +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..1227a1c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verification_tokens_token_uq_idx` ON `identity_verification_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..0eb6954 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000001_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verification_tokens_token_uq_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..d0650f3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_token_idx` ON `identity_verification_tokens` (`token`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..2b817e7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000002_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_token_idx" ON "identity_verification_tokens" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..c3a3c47 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verifiable_address_id_idx` ON `identity_verification_tokens` (`identity_verifiable_address_id`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..d43604b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000003_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verifiable_address_id_idx" ON "identity_verification_tokens" (identity_verifiable_address_id) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.cockroach.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql new file mode 100644 index 0000000..e134442 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.mysql.up.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verification_tokens_verification_flow_id_idx` ON `identity_verification_tokens` (`selfservice_verification_flow_id`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.postgres.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql new file mode 100644 index 0000000..c0eac25 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830154602000004_add_verification_token.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verification_tokens_verification_flow_id_idx" ON "identity_verification_tokens" (selfservice_verification_flow_id); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..ed9ee13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..80ec024 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `issued_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..0da7c73 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `expires_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..5865f64 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "issued_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..ed9ee13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..b227755 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000000_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..425c694 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..74e835a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..91dd9b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` DROP COLUMN `expires_at` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..5e69d7a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` ADD COLUMN `issued_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..425c694 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "expires_at" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..74e835a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d5b864c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..cdb8f9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000001_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..0ada192 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..c8953a2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..712d8b2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..0fe490d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_recovery_tokens` MODIFY `selfservice_recovery_flow_id` char(36); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..10e628a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..a4605d0 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" TYPE UUID, ALTER COLUMN "selfservice_recovery_flow_id" DROP NOT NULL; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..6670ad2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000002_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..7ddc5ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..d3ad85b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000003_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..e09ac77 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ALTER COLUMN "selfservice_recovery_flow_id" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..479627d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..55082d4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000004_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36), +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..ab73f48 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..ab73f48 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +UPDATE "identity_recovery_tokens" SET "selfservice_recovery_flow_id" = "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..7059164 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000005_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..479627d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD COLUMN "selfservice_recovery_flow_id" UUID \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..7ddc5ce --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP COLUMN "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000006_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..d3ad85b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" RENAME COLUMN "selfservice_recovery_flow_id" TO "_selfservice_recovery_flow_id_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..c15f380 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" ADD CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" FOREIGN KEY ("selfservice_recovery_flow_id") REFERENCES "selfservice_recovery_flows" ("id") ON UPDATE NO ACTION ON DELETE CASCADE; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..6557e4c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000007_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..c8953a2 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_recovery_tokens" DROP CONSTRAINT "identity_recovery_tokens_selfservice_recovery_requests_id_fk" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..c4ea5d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000008_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d5b864c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..1ebb1c9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000009_recovery_token_expires.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ab19747 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000010_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000011_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000012_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..92460b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000013_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000014_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000015_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..c4ea5d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_recovery_tokens_tmp" RENAME TO "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000016_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddb21ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000017_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..6557e4c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_recovery_tokens_tmp" (id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at) SELECT id, token, used, used_at, identity_recovery_address_id, selfservice_recovery_flow_id, created_at, updated_at, expires_at, issued_at FROM "identity_recovery_tokens" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000018_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f6b4471 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_recovery_addresses_code_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000019_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..d278d9c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_recovery_addresses_code_uq_idx" ON "_identity_recovery_tokens_tmp" (token) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000020_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..4c6931a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_recovery_tokens_tmp" ( +"id" TEXT PRIMARY KEY, +"token" TEXT NOT NULL, +"used" bool NOT NULL DEFAULT 'false', +"used_at" DATETIME, +"identity_recovery_address_id" char(36) NOT NULL, +"selfservice_recovery_flow_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"expires_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +"issued_at" DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00', +FOREIGN KEY (identity_recovery_address_id) REFERENCES identity_recovery_addresses (id) ON UPDATE NO ACTION ON DELETE CASCADE, +FOREIGN KEY (selfservice_recovery_flow_id) REFERENCES selfservice_recovery_flows (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000021_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..ddbeebc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000022_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..f3e151a --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_recovery_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000023_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql new file mode 100644 index 0000000..af8197e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_recovery_tokens WHERE selfservice_recovery_flow_id IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200830172221000024_recovery_token_expires.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..037a822 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..788e152 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` (`code`); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..9ac004d --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..fcf23d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..4adfcd1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_code_idx" ON "identity_verifiable_addresses" (code); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..037a822 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000000_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..ab06acf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..3f787b6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX `identity_verifiable_addresses_code_uq_idx` ON `identity_verifiable_addresses` (`code`) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..6f6853c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +DROP INDEX `identity_verifiable_addresses_code_idx` ON `identity_verifiable_addresses` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..16f5505 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +DROP INDEX "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..ecf8ba9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_code_uq_idx" ON "identity_verifiable_addresses" (code) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..ab06acf --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000001_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_code_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..a9426a4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..b1d2ffc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..1ca3bf9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `expires_at` DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..4cde445 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `code` \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..2042f8f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "expires_at" TYPE timestamp, ALTER COLUMN "expires_at" DROP NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..b1d2ffc --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "code" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000002_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..86dce13 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "expires_at" = "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.cockroach.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..76be8ed --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` MODIFY `code` VARCHAR (32) NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e58903e --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` DROP COLUMN `expires_at`; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..47f8bb6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" TYPE VARCHAR (32), ALTER COLUMN "code" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..6c491ea --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.postgres.up.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "expires_at"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f093d32 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000003_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..07227f5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e9f0577 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000004_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"expires_at" DATETIME NOT NULL DEFAULT 'CURRENT_TIMESTAMP', +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..98f6fd8 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "expires_at" TO "_expires_at_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..717d0e3 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = LEFT(SHA2(RANDOM_BYTES(32), 256), 32) WHERE code IS NULL diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..999d350 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(random()::text), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000005_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..2e16a02 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" DROP COLUMN "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..a005a81 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `expires_at` DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000006_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..b96c559 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ALTER COLUMN "code" SET NOT NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql new file mode 100644 index 0000000..8c367bb --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.down.sql @@ -0,0 +1 @@ +ALTER TABLE `identity_verifiable_addresses` ADD COLUMN `code` VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.mysql.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.postgres.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..baf8813 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..289889f --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000007_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, expires_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..ab970e1 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE "identity_verifiable_addresses" SET "code" = "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..b9252d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000008_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000009_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..6fb5802 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" RENAME COLUMN "code" TO "_code_tmp" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6d810ee --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000010_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..f093d32 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000011_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..d496bf4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(md5(uuid_v4()), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..07227f5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at, code, expires_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e5b27b4 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000012_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..fc85347 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" timestamp \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..6165df6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000013_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE INDEX "identity_verifiable_addresses_status_via_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql new file mode 100644 index 0000000..8e366ec --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" VARCHAR (32) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.cockroach.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..fca4711 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000014_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +CREATE UNIQUE INDEX "identity_verifiable_addresses_status_via_uq_idx" ON "_identity_verifiable_addresses_tmp" (via, value) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..baf8813 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1,14 @@ +CREATE TABLE "_identity_verifiable_addresses_tmp" ( +"id" TEXT PRIMARY KEY, +"status" TEXT NOT NULL, +"via" TEXT NOT NULL, +"verified" bool NOT NULL, +"value" TEXT NOT NULL, +"verified_at" DATETIME, +"identity_id" char(36) NOT NULL, +"created_at" DATETIME NOT NULL, +"updated_at" DATETIME NOT NULL, +"code" TEXT NOT NULL, +"expires_at" DATETIME, +FOREIGN KEY (identity_id) REFERENCES identities (id) ON UPDATE NO ACTION ON DELETE CASCADE +) \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..759f782 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000015_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO "_identity_verifiable_addresses_tmp" (id, status, via, verified, value, verified_at, identity_id, created_at, updated_at) SELECT id, status, via, verified, value, verified_at, identity_id, created_at, updated_at FROM "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..57acd91 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..b9252d5 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000016_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1,2 @@ + +DROP TABLE "identity_verifiable_addresses" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..9fb21b7 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +DROP INDEX IF EXISTS "identity_verifiable_addresses_status_via_uq_idx" \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..3ae0041 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000017_identity_verifiable_address_remove_code.sqlite3.up.sql @@ -0,0 +1 @@ +ALTER TABLE "_identity_verifiable_addresses_tmp" RENAME TO "identity_verifiable_addresses"; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..d86919b --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET expires_at = CURRENT_TIMESTAMP WHERE expires_at IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000018_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..a8693e6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +UPDATE identity_verifiable_addresses SET code = substr(hex(randomblob(32)), 0, 32) WHERE code IS NULL \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000019_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..21462d6 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "expires_at" DATETIME \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000020_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql new file mode 100644 index 0000000..b73f215 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.down.sql @@ -0,0 +1 @@ +ALTER TABLE "identity_verifiable_addresses" ADD COLUMN "code" TEXT \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20200831110752000021_identity_verifiable_address_remove_code.sqlite3.up.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..a2e136c --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.down.sql @@ -0,0 +1 @@ +DELETE FROM identity_credential_types WHERE name = 'password' OR name = 'oidc'; \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..d94b0a9 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000000_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '78c1b41d-8341-4507-aa60-aff1d4369670', 'password' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'password') \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.cockroach.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.mysql.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.postgres.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.down.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.down.sql new file mode 100644 index 0000000..e69de29 diff --git a/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql new file mode 100644 index 0000000..de26838 --- /dev/null +++ b/oryx/popx/stub/migrations/transactional/20201201161451000001_credential_types_values.sqlite3.up.sql @@ -0,0 +1 @@ +INSERT INTO identity_credential_types (id, name) SELECT '6fa5e2e0-bfce-4631-b62b-cf2b0252b289', 'oidc' WHERE NOT EXISTS ( SELECT * FROM identity_credential_types WHERE name = 'oidc'); \ No newline at end of file diff --git a/oryx/popx/transaction.go b/oryx/popx/transaction.go new file mode 100644 index 0000000..0e69c70 --- /dev/null +++ b/oryx/popx/transaction.go @@ -0,0 +1,255 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package popx + +import ( + "context" + "database/sql" + "fmt" + "math/rand/v2" + "runtime" + "strings" + "time" + + "github.com/cockroachdb/cockroach-go/v2/crdb" + "github.com/jmoiron/sqlx" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + + "github.com/ory/x/prometheusx" + + "github.com/ory/pop/v6" + "github.com/ory/x/sqlcon" +) + +type transactionContextKey int + +const transactionKey transactionContextKey = 0 + +func WithTransaction(ctx context.Context, tx *pop.Connection) context.Context { + return context.WithValue(ctx, transactionKey, tx) +} + +func InTransaction(ctx context.Context) bool { + return ctx.Value(transactionKey) != nil +} + +func Transaction(ctx context.Context, connection *pop.Connection, callback func(context.Context, *pop.Connection) error) error { + return TransactionWithOptions(ctx, connection, nil, callback) +} + +// TransactionWithOptions opens the transaction with given sql.TxOptions, allowing isolation level to be set. +func TransactionWithOptions(ctx context.Context, connection *pop.Connection, opts *sql.TxOptions, callback func(context.Context, *pop.Connection) error) error { + c := ctx.Value(transactionKey) + if c != nil { + if conn, ok := c.(*pop.Connection); ok { + return errors.WithStack(callback(ctx, conn.WithContext(ctx))) + } + } + + conn := connection.WithContext(ctx) + + switch conn.Dialect.Name() { + case "cockroach": + return conn.Dialect.Lock(func() error { + tx, err := conn.NewTransactionContextOptions(ctx, opts) + if err != nil { + return errors.WithStack(err) + } + attempt := 0 + // A transaction's retries are recorded in a single increment so + // that the trace exemplar's value equals the transaction's retry + // count, which Grafana renders as the exemplar marker's position. + // Deferred so that retries are recorded even when the callback + // panics. + defer func() { + if retries := attempt - 1; retries > 0 { + recordRetries(ctx, retries) + } + }() + return errors.WithStack(crdb.ExecuteInTx(ctx, sqlxTxAdapter{tx.TX.Tx}, func() error { + attempt++ + return errors.WithStack(callback(WithTransaction(ctx, tx), tx)) + })) + }) + case "postgres", "mysql", "yugabyte": + // YugabyteDB speaks the Postgres wire protocol and, like Postgres, may + // surface serialization failures (SQLSTATE 40001) that the retry loop + // below handles through sqlcon.ErrConcurrentUpdate. + // + // Mirrors pop's Connection#Transaction with opts passed to NewTransactionContextOptions. + // https://github.com/ory/pop/blob/89126558d36911217a1cc70172ba94ee32692cad/connection.go#L148 + return conn.Dialect.Lock(func() error { + var err error + for range MaxTransactionRetries { + err = func() error { + cn, err := conn.NewTransactionContextOptions(ctx, opts) + if err != nil { + return errors.WithStack(err) + } + defer func() { + if ex := recover(); ex != nil { + _ = cn.TX.Rollback() + panic(ex) + } + }() + err = callback(WithTransaction(ctx, cn), cn) + var dberr error + if err != nil { + dberr = cn.TX.Rollback() + if errors.Is(dberr, sql.ErrTxDone) { + // Already rolled back by the database (e.g. context cancelled). + return err + } + if dberr != nil && dberr.Error() == "conn closed" { + // pgx closes the connection on context cancellation before + // database/sql gets a chance to roll back. + // See https://github.com/jackc/pgx/issues/2551 + return err + } + } else { + dberr = cn.TX.Commit() + } + if dberr != nil { + return fmt.Errorf("database error on committing or rolling back transaction: %w", dberr) + } + return err + }() + if err == nil || !errors.Is(sqlcon.HandleError(err), sqlcon.ErrConcurrentUpdate()) { + return err + } + } + return err + }) + } + + // SQLite and unknown dialects: opts are ignored; use pop's default + // transaction path with concurrent-update retry handling. + var err error + for attempt := range MaxTransactionRetries { + err = conn.Transaction(func(tx *pop.Connection) error { + return callback(WithTransaction(ctx, tx), tx) + }) + if err == nil { + return nil + } + if !errors.Is(sqlcon.HandleError(err), sqlcon.ErrConcurrentUpdate()) { + return err + } + // Back off with jitter before retrying. SQLite in WAL mode fails a + // deferred transaction's read-to-write upgrade with + // SQLITE_BUSY_SNAPSHOT immediately (busy_timeout cannot help a stale + // snapshot), so an immediate retry under write contention can livelock + // through the whole retry budget. + select { + case <-ctx.Done(): + return err + case <-time.After(time.Duration(attempt+1) * time.Duration(1+rand.IntN(3)) * time.Millisecond): + } + } + return err +} + +func GetConnection(ctx context.Context, connection *pop.Connection) *pop.Connection { + c := ctx.Value(transactionKey) + if c != nil { + if conn, ok := c.(*pop.Connection); ok { + return conn.WithContext(ctx) + } + } + return connection.WithContext(ctx) +} + +type sqlxTxAdapter struct { + *sqlx.Tx +} + +var _ crdb.Tx = sqlxTxAdapter{} + +func (s sqlxTxAdapter) Exec(ctx context.Context, query string, args ...any) error { + _, err := s.Tx.ExecContext(ctx, query, args...) + return errors.WithStack(err) +} + +func (s sqlxTxAdapter) Commit(ctx context.Context) error { + return errors.WithStack(s.Tx.Commit()) +} + +func (s sqlxTxAdapter) Rollback(ctx context.Context) error { + return errors.WithStack(s.Tx.Rollback()) +} + +// MaxTransactionRetries is the number of times a transaction is retried on +// a concurrent-update conflict before the error is returned to the caller. +const MaxTransactionRetries = 10 + +var ( + transactionRetries = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "ory_x_popx_cockroach_transaction_retries_total", + Help: "Counts the number of automatic CockroachDB transaction retries", + }, []string{"caller"}) + TransactionRetries prometheus.Collector = transactionRetries + _ = transactionRetries.WithLabelValues(unknownCaller) // make sure the metric is always present + unknownCaller = "unknown" +) + +// recordRetries counts a transaction's automatic CockroachDB restarts. When +// the surrounding request is traced, it also attaches the trace to the span +// (as an event) and to the counter (as an exemplar whose value is the retry +// count) so that retry spikes can be linked to concrete requests. +func recordRetries(ctx context.Context, retries int) { + c := caller() + counter := transactionRetries.WithLabelValues(c) + span := trace.SpanFromContext(ctx) + span.AddEvent("db.transaction.retry", trace.WithAttributes( + attribute.String("caller", c), + attribute.Int("retries", retries), + )) + prometheusx.AddWithExemplar(ctx, counter, float64(retries)) +} + +// caller returns the function that opened the transaction: the innermost +// frame that is neither transaction plumbing (this package, pop, and +// crdb.ExecuteInTx) nor one of the thin wrapper methods that services layer +// over popx, so that a retry is attributed to the business operation rather +// than to a generic Transaction wrapper. +func caller() string { + pc := make([]uintptr, 32) + n := runtime.Callers(2, pc) + frames := runtime.CallersFrames(pc[:n]) + for { + frame, more := frames.Next() + if frame.Function != "" && !skipCallerFrame(frame.Function) { + return frame.Function + } + if !more { + return unknownCaller + } + } +} + +// skipCallerFrame reports whether a stack frame identifies transaction +// plumbing rather than the transaction's origin: this package and the +// libraries under it, and the thin wrapper methods services layer over popx, +// e.g. hydra's BasePersister.Transaction and RegistrySQL.Transaction +// (fosite's Transactional interface), keto's WithLatestSnapshot, and +// backoffice's WithFollowerReads. +func skipCallerFrame(fn string) bool { + return strings.HasPrefix(fn, "github.com/ory/x/popx.") || + strings.HasPrefix(fn, "github.com/ory/pop/") || + strings.HasPrefix(fn, "github.com/cockroachdb/cockroach-go/v2/crdb.ExecuteInTx") || + // Deferred recording runs with runtime frames (e.g. runtime.gopanic) + // on the stack when the transaction callback panicked. + strings.HasPrefix(fn, "runtime.") || + // Wrapper matching is deliberately coarse: a frame that merely + // mentions one of these names is skipped and the retry is attributed + // one frame further up, which is still a useful label. "Transaction" + // also covers TransactionWithOptions and method-value "-fm" symbols. + strings.Contains(fn, "Transaction") || + strings.Contains(fn, "WithLatestSnapshot") || + strings.Contains(fn, "WithFollowerReads") +} diff --git a/oryx/profilex/profiling.go b/oryx/profilex/profiling.go new file mode 100644 index 0000000..3e143e5 --- /dev/null +++ b/oryx/profilex/profiling.go @@ -0,0 +1,40 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package profilex + +import ( + "os" + + "github.com/pkg/profile" +) + +type noop struct{} + +// Stop is a noop. +func (p *noop) Stop() {} + +// Profile parses the PROFILING environment variable and executes the proper profiling task. +func Profile() interface { + Stop() +} { + switch os.Getenv("PROFILING") { + case "cpu": + return profile.Start(profile.CPUProfile, profile.NoShutdownHook) + case "mem": + return profile.Start(profile.MemProfile, profile.NoShutdownHook) + case "mutex": + return profile.Start(profile.MutexProfile, profile.NoShutdownHook) + case "block": + return profile.Start(profile.BlockProfile, profile.NoShutdownHook) + } + return new(noop) +} + +// HelpMessage returns a string explaining how profiling works. +func HelpMessage() string { + return `- PROFILING: Set "PROFILING=cpu" to enable cpu profiling and "PROFILING=mem" to enable memory profiling. + It is not possible to do both at the same time. Profiling is disabled per default. + + Example: PROFILING=cpu` +} diff --git a/oryx/prometheusx/exemplar.go b/oryx/prometheusx/exemplar.go new file mode 100644 index 0000000..e9b87b3 --- /dev/null +++ b/oryx/prometheusx/exemplar.go @@ -0,0 +1,61 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" +) + +// ExemplarFromContext returns exemplar labels (trace_id, span_id) for the +// sampled trace span in ctx, or nil when there is none. The nil return makes +// it directly usable with promhttp.WithExemplarFromContext. +func ExemplarFromContext(ctx context.Context) prometheus.Labels { + sc := trace.SpanContextFromContext(ctx) + if !sc.IsValid() || !sc.IsSampled() { + return nil + } + return prometheus.Labels{ + "trace_id": sc.TraceID().String(), + "span_id": sc.SpanID().String(), + } +} + +// AddWithExemplar adds v to counter. When ctx carries a sampled trace span, +// the addition carries a trace exemplar so dashboards can link the counter +// to the trace. Any panic from exemplar validation is swallowed: a lost +// exemplar must never break the recording path, and client_golang applies +// the addition before validating the exemplar, so the count is recorded +// either way. +func AddWithExemplar(ctx context.Context, counter prometheus.Counter, v float64) { + exemplar := ExemplarFromContext(ctx) + adder, ok := counter.(prometheus.ExemplarAdder) + if exemplar == nil || !ok { + counter.Add(v) + return + } + defer func() { + _ = recover() + }() + adder.AddWithExemplar(v, exemplar) +} + +// ObserveWithExemplar records v on obs. When ctx carries a sampled trace +// span, the observation carries a trace exemplar on the histogram bucket it +// falls into. The same panic-swallowing rationale as AddWithExemplar +// applies. +func ObserveWithExemplar(ctx context.Context, obs prometheus.Observer, v float64) { + exemplar := ExemplarFromContext(ctx) + observer, ok := obs.(prometheus.ExemplarObserver) + if exemplar == nil || !ok { + obs.Observe(v) + return + } + defer func() { + _ = recover() + }() + observer.ObserveWithExemplar(v, exemplar) +} diff --git a/oryx/prometheusx/handler.go b/oryx/prometheusx/handler.go new file mode 100644 index 0000000..aac59e7 --- /dev/null +++ b/oryx/prometheusx/handler.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +const ( + MetricsPrometheusPath = "/metrics/prometheus" +) + +type muxrouter interface { + Handle(path string, handle http.Handler) +} + +// Handler returns the metrics handler. It mirrors promhttp.Handler() but +// additionally serves the OpenMetrics format when the scraper negotiates it, +// because that is the only exposition format that carries exemplars (trace +// links on counters and histograms). It is safe to call multiple times. +func Handler() http.Handler { + return promhttp.InstrumentMetricHandler( + prometheus.DefaultRegisterer, + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{ + EnableOpenMetrics: true, + }), + ) +} + +// SetMuxRoutes registers the prometheus handler. +func SetMuxRoutes(mux muxrouter) { + mux.Handle("GET "+MetricsPrometheusPath, Handler()) +} diff --git a/oryx/prometheusx/metrics.go b/oryx/prometheusx/metrics.go new file mode 100644 index 0000000..669c9d9 --- /dev/null +++ b/oryx/prometheusx/metrics.go @@ -0,0 +1,205 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import ( + "net/http" + "regexp" + "strings" + "sync" + "time" + + grpcPrometheus "github.com/grpc-ecosystem/go-grpc-prometheus" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/urfave/negroni" + + "github.com/ory/x/httprouterx" +) + +type HTTPMetrics struct { + responseTime *prometheus.HistogramVec + totalRequests *prometheus.CounterVec + duration *prometheus.HistogramVec + responseSize *prometheus.HistogramVec + requestSize *prometheus.HistogramVec + handlerStatuses *prometheus.CounterVec +} + +const HTTPPrefix = "http" + +func NewHTTPMetrics(app, metricsPrefix, version, hash, date string) *HTTPMetrics { + labels := map[string]string{ + "app": app, + "version": version, + "hash": hash, + "buildTime": date, + } + + if metricsPrefix != "" { + metricsPrefix += "_" + } + + pm := &HTTPMetrics{ + responseTime: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "response_time_seconds", + Help: "Description", + ConstLabels: labels, + }, []string{"endpoint"}), + totalRequests: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: metricsPrefix + "requests_total", + Help: "number of requests", + ConstLabels: labels, + }, []string{"code", "method", "endpoint"}), + duration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "requests_duration_seconds", + Help: "duration of a requests in seconds", + ConstLabels: labels, + }, []string{"code", "method", "endpoint"}), + responseSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "response_size_bytes", + Help: "size of the responses in bytes", + ConstLabels: labels, + }, []string{"code", "method"}), + requestSize: prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: metricsPrefix + "requests_size_bytes", + Help: "size of the requests in bytes", + ConstLabels: labels, + }, []string{"code", "method"}), + handlerStatuses: prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: metricsPrefix + "requests_statuses_total", + Help: "count number of responses per status", + ConstLabels: labels, + }, []string{"method", "status_bucket"}), + } + + err := prometheus.Register(pm) + if e := new(prometheus.AlreadyRegisteredError); errors.As(err, e) { + return pm + } else if err != nil { + panic(err) + } + + grpcPrometheus.EnableHandlingTimeHistogram() + + return pm +} + +// Describe implements prometheus Collector interface. +func (h *HTTPMetrics) Describe(in chan<- *prometheus.Desc) { + h.duration.Describe(in) + h.totalRequests.Describe(in) + h.requestSize.Describe(in) + h.responseSize.Describe(in) + h.handlerStatuses.Describe(in) + h.responseTime.Describe(in) +} + +// Collect implements prometheus Collector interface. +func (h *HTTPMetrics) Collect(in chan<- prometheus.Metric) { + h.duration.Collect(in) + h.totalRequests.Collect(in) + h.requestSize.Collect(in) + h.responseSize.Collect(in) + h.handlerStatuses.Collect(in) + h.responseTime.Collect(in) +} + +func (h *HTTPMetrics) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + rr := negroni.NewResponseWriter(rw) + start := time.Now() + + var pattern string + next(rr, httprouterx.WithAfterMatchHook(r, func(r *http.Request) { + pattern = r.Pattern + })) + + latency := time.Since(start) + code := StatusCodeToString(rr.Status()) + method := sanitizeMethod(r.Method) + endpoint := getLabelForPattern(pattern) + + // Latency histograms and request counters carry trace exemplars so that + // dashboards can link latency outliers and error spikes to traces. The + // request's span is still open here: the tracing middleware wraps this + // one. + ctx := r.Context() + h.responseSize.WithLabelValues(code, method).Observe(float64(rr.Size())) + AddWithExemplar(ctx, h.totalRequests.WithLabelValues(code, method, endpoint), 1) + ObserveWithExemplar(ctx, h.duration.WithLabelValues(code, method, endpoint), latency.Seconds()) + ObserveWithExemplar(ctx, h.responseTime.WithLabelValues(endpoint), latency.Seconds()) + h.requestSize.WithLabelValues(code, method).Observe(float64(computeApproximateRequestSize(r))) + + statusBucket := "unknown" + switch status := rr.Status(); { + case status >= 200 && status <= 299: + statusBucket = "2xx" + case status >= 300 && status <= 399: + statusBucket = "3xx" + case status >= 400 && status <= 499: + statusBucket = "4xx" + case status >= 500 && status <= 599: + statusBucket = "5xx" + } + + AddWithExemplar(ctx, h.handlerStatuses.WithLabelValues(r.Method, statusBucket), 1) +} + +var ( + paramPlaceHolderRE = regexp.MustCompile(`\{[a-zA-Z0-9_-]+}`) + // patternLabelCache is used to cache the generated label for a given pattern to avoid recomputing it on every request. + // We use a sync.Map over a ristretto cache as the number of unique patterns is expected to be low and the cache will not grow indefinitely. + // + // > The sync.Map type is optimized for two common use cases: (1) when the entry for a given + // > key is only ever written once but read many times, as in caches that only grow, + // > or (2) when multiple goroutines read, write, and overwrite entries for disjoint + // > sets of keys. In these two cases, use of a Map may significantly reduce lock + // > contention compared to a Go map paired with a separate [Mutex] or [RWMutex]. + // https://pkg.go.dev/sync#Map + patternLabelCache = sync.Map{} +) + +func getLabelForPattern(pattern string) string { + if label, ok := patternLabelCache.Load(pattern); ok { + return label.(string) + } + + cleanedPattern := pattern + // remove the method if it is included + if _, pattern, ok := strings.Cut(cleanedPattern, " "); ok { + cleanedPattern = pattern + } + // trim space, just to be sure + cleanedPattern = strings.TrimSpace(cleanedPattern) + + label := paramPlaceHolderRE.ReplaceAllString(strings.TrimSuffix(cleanedPattern, "/{$}"), "{param}") + // Cache the generated label for the pattern to avoid recomputing it on every request. + patternLabelCache.Store(pattern, label) + return label +} + +// computeApproximateRequestSize is copied from the promhttp package as it is not exported +func computeApproximateRequestSize(r *http.Request) int { + s := 0 + if r.URL != nil { + s += len(r.URL.String()) + } + + s += len(r.Method) + s += len(r.Proto) + for name, values := range r.Header { + s += len(name) + for _, value := range values { + s += len(value) + } + } + s += len(r.Host) + + // N.B. r.Form and r.MultipartForm are assumed to be included in r.URL. + + if r.ContentLength != -1 { + s += int(r.ContentLength) + } + return s +} diff --git a/oryx/prometheusx/sanitize_method.go b/oryx/prometheusx/sanitize_method.go new file mode 100644 index 0000000..85905ff --- /dev/null +++ b/oryx/prometheusx/sanitize_method.go @@ -0,0 +1,31 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +func sanitizeMethod(m string) string { + switch m { + case "GET", "get": + return "get" + case "PUT", "put": + return "put" + case "HEAD", "head": + return "head" + case "POST", "post": + return "post" + case "DELETE", "delete": + return "delete" + case "CONNECT", "connect": + return "connect" + case "OPTIONS", "options": + return "options" + case "NOTIFY", "notify": + return "notify" + case "TRACE", "trace": + return "trace" + case "PATCH", "patch": + return "patch" + default: + return "unknown" + } +} diff --git a/oryx/prometheusx/status_code_strings.go b/oryx/prometheusx/status_code_strings.go new file mode 100644 index 0000000..7545217 --- /dev/null +++ b/oryx/prometheusx/status_code_strings.go @@ -0,0 +1,32 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package prometheusx + +import "strconv" + +var toString = [600]string{ + 100: "100", 101: "101", 102: "102", 103: "103", + + 200: "200", 201: "201", 202: "202", 203: "203", 204: "204", 205: "205", 206: "206", 207: "207", 208: "208", + 226: "226", + + 300: "300", 301: "301", 302: "302", 303: "303", 304: "304", 305: "305", 307: "307", 308: "308", + 400: "400", 401: "401", 402: "402", 403: "403", 404: "404", 405: "405", 406: "406", 407: "407", 408: "408", 409: "409", 410: "410", 411: "411", 412: "412", 413: "413", 414: "414", 415: "415", 416: "416", 417: "417", 418: "418", 421: "421", 422: "422", 423: "423", 424: "424", 425: "425", 426: "426", 428: "428", 429: "429", + 431: "431", + 451: "451", + 499: "499", + + 500: "500", 501: "501", 502: "502", 503: "503", 504: "504", 505: "505", 506: "506", 507: "507", 508: "508", 510: "510", 511: "511", +} + +// StatusCodeToString converts an HTTP status code to its string representation. +// It is highly optimized for the well-known HTTP status codes. +func StatusCodeToString(code int) string { + if code >= 0 && code < len(toString) { + if s := toString[code]; s != "" { + return s + } + } + return strconv.Itoa(code) +} diff --git a/oryx/proxy/proxy.go b/oryx/proxy/proxy.go new file mode 100644 index 0000000..f379baf --- /dev/null +++ b/oryx/proxy/proxy.go @@ -0,0 +1,321 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package proxy + +import ( + "context" + "log" + "net/http" + "net/http/httputil" + + "github.com/pkg/errors" + + "github.com/rs/cors" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +type ( + RespMiddleware func(resp *http.Response, config *HostConfig, body []byte) ([]byte, error) + ReqMiddleware func(req *httputil.ProxyRequest, config *HostConfig, body []byte) ([]byte, error) + HostMapper func(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error) + options struct { + hostMapper HostMapper + onResError func(*http.Response, error) error + onReqError func(*http.Request, error) + respMiddlewares []RespMiddleware + reqMiddlewares []ReqMiddleware + transport http.RoundTripper + errHandler func(http.ResponseWriter, *http.Request, error) + } + HostConfig struct { + // CorsEnabled is a flag to enable or disable CORS + // Default: false + CorsEnabled bool + // CorsOptions allows to configure CORS + // If left empty, no CORS headers will be set even when CorsEnabled is true + CorsOptions *cors.Options + // CookieDomain is the host under which cookies are set. + // If left empty, no cookie domain will be set + CookieDomain string + // UpstreamHost is the next upstream host the proxy will pass the request to. + // e.g. fluffy-bear-afiu23iaysd.oryapis.com + UpstreamHost string + // UpstreamScheme is the protocol used by the upstream service. + UpstreamScheme string + // TargetHost is the final target of the request. Should be the same as UpstreamHost + // if the request is directly passed to the target service. + TargetHost string + // TargetScheme is the final target's scheme + // (i.e. the scheme the target thinks it is running under) + TargetScheme string + // PathPrefix is a prefix that is prepended on the original host, + // but removed before forwarding. + PathPrefix string + // TrustForwardedHosts is a flag that indicates whether the proxy should trust the + // X-Forwarded-* headers or not. + TrustForwardedHeaders bool + // originalHost the original hostname the request is coming from. + // This value will be maintained internally by the proxy. + originalHost string + // originalScheme is the original scheme of the request. + // This value will be maintained internally by the proxy. + originalScheme string + // ForceOriginalSchemeHTTP forces the original scheme to be https if enabled. + ForceOriginalSchemeHTTPS bool + } + Options func(*options) + contextKey string +) + +const ( + hostConfigKey contextKey = "host config" + serverSpanKey contextKey = "server span" +) + +func (c *HostConfig) setScheme(r *httputil.ProxyRequest) { + if c.ForceOriginalSchemeHTTPS { + c.originalScheme = "https" + } else if forwardedProto := r.In.Header.Get("X-Forwarded-Proto"); forwardedProto != "" { + c.originalScheme = forwardedProto + } else if r.In.TLS == nil { + c.originalScheme = "http" + } else { + c.originalScheme = "https" + } +} + +func (c *HostConfig) setHost(r *httputil.ProxyRequest) { + if forwardedHost := r.In.Header.Get("X-Forwarded-Host"); forwardedHost != "" { + c.originalHost = forwardedHost + } else { + c.originalHost = r.In.Host + } +} + +// rewriter is a custom internal function for altering a http.Request +func rewriter(o *options) func(*httputil.ProxyRequest) { + return func(r *httputil.ProxyRequest) { + // Stash the server-side span so that modifyResponse can restore it as + // the active span before the response middlewares run. Without a valid + // span there is nothing worth restoring: replacing the transport's + // client span with a noop span would orphan the middleware spans. + ctx := r.Out.Context() + if s := trace.SpanFromContext(ctx); s.SpanContext().IsValid() { + ctx = context.WithValue(ctx, serverSpanKey, s) + r.Out = r.Out.WithContext(ctx) + } + + ctx, span := otel.GetTracerProvider().Tracer("").Start(ctx, "x.proxy") + defer span.End() + + _, c, err := o.getHostConfig(ctx, r.In) + if err != nil { + o.onReqError(r.Out, err) + return + } + + if c.TrustForwardedHeaders { + headers := []string{ + "X-Forwarded-Host", + "X-Forwarded-Proto", + "X-Forwarded-For", + } + for _, h := range headers { + if v := r.In.Header.Get(h); v != "" { + r.Out.Header.Set(h, v) + } + } + } + + c.setScheme(r) + c.setHost(r) + + headerRequestRewrite(r.Out, c) + + var body []byte + var cb *compressableBody + + if r.Out.ContentLength != 0 { + body, cb, err = readBody(r.Out.Header, r.Out.Body) + if err != nil { + o.onReqError(r.Out, err) + return + } + } + + for _, m := range o.reqMiddlewares { + if body, err = m(r, c, body); err != nil { + o.onReqError(r.Out, err) + return + } + } + + n, err := cb.Write(body) + if err != nil { + o.onReqError(r.Out, err) + return + } + + r.Out.Header.Del("Content-Length") + r.Out.ContentLength = int64(n) + r.Out.Body = cb + } +} + +// modifyResponse is a custom internal function for altering a http.Response +func modifyResponse(o *options) func(*http.Response) error { + return func(r *http.Response) error { + // An instrumented transport (otelhttp) clones the outbound request with + // its HTTP client span as the active span and ends that span once the + // response body is consumed. Restore the server-side span stashed in + // rewriter so that spans started by the response middlewares are + // parented to the still-open server span instead of the ended client + // span. + if span, ok := r.Request.Context().Value(serverSpanKey).(trace.Span); ok { + r.Request = r.Request.WithContext(trace.ContextWithSpan(r.Request.Context(), span)) + } + + _, c, err := o.getHostConfig(r.Request.Context(), r.Request) + if err != nil { + return err + } + + if err := headerResponseRewrite(r, c); err != nil { + return o.onResError(r, err) + } + + body, cb, err := bodyResponseRewrite(r, c) + if err != nil { + return o.onResError(r, err) + } + + for _, m := range o.respMiddlewares { + if body, err = m(r, c, body); err != nil { + return o.onResError(r, err) + } + } + + n, err := cb.Write(body) + if err != nil { + return o.onResError(r, err) + } + + n, t, err := handleWebsocketResponse(n, cb, r.Body) + if err != nil { + return err + } + + r.Header.Del("Content-Length") + r.ContentLength = int64(n) + r.Body = t + return nil + } +} + +func WithOnError(onReqErr func(*http.Request, error), onResErr func(*http.Response, error) error) Options { + return func(o *options) { + o.onReqError = onReqErr + o.onResError = onResErr + } +} + +func WithReqMiddleware(middlewares ...ReqMiddleware) Options { + return func(o *options) { + o.reqMiddlewares = append(o.reqMiddlewares, middlewares...) + } +} + +func WithRespMiddleware(middlewares ...RespMiddleware) Options { + return func(o *options) { + o.respMiddlewares = append(o.respMiddlewares, middlewares...) + } +} + +func WithTransport(t http.RoundTripper) Options { + return func(o *options) { + o.transport = t + } +} + +func WithErrorHandler(eh func(w http.ResponseWriter, r *http.Request, err error)) Options { + return func(o *options) { + o.errHandler = eh + } +} + +func (o *options) getHostConfig(ctx context.Context, r *http.Request) (context.Context, *HostConfig, error) { + if cached, ok := ctx.Value(hostConfigKey).(*HostConfig); ok && cached != nil { + return ctx, cached, nil + } + ctx, c, err := o.hostMapper(ctx, r) + if err != nil { + return nil, nil, err + } + // cache the host config in the request context + // this will be passed on to the request and response proxy functions + ctx = context.WithValue(ctx, hostConfigKey, c) + return ctx, c, nil +} + +func (o *options) beforeProxyMiddleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + // get the hostmapper configurations before the request is proxied + ctx, c, err := o.getHostConfig(request.Context(), request) + if err != nil { + o.onReqError(request, err) + return + } + + // Add our Cors middleware. + // This middleware will only trigger if the host config has cors enabled on that request. + if c.CorsEnabled && c.CorsOptions != nil { + cors.New(*c.CorsOptions).HandlerFunc(writer, request) + } + + h.ServeHTTP(writer, request.WithContext(ctx)) + }) +} + +func defaultErrorHandler(w http.ResponseWriter, r *http.Request, err error) { + switch { + case errors.Is(err, context.Canceled): + w.WriteHeader(499) // http://nginx.org/en/docs/dev/development_guide.html + case isTimeoutError(err): + w.WriteHeader(http.StatusGatewayTimeout) + default: + log.Printf("http: proxy error: %v", err) + w.WriteHeader(http.StatusBadGateway) + } +} + +func isTimeoutError(err error) bool { + var te interface{ Timeout() bool } = nil + return errors.As(err, &te) && te.Timeout() || errors.Is(err, context.DeadlineExceeded) +} + +// New creates a new Proxy +// A Proxy sets up a middleware with custom request and response modification handlers +func New(hostMapper HostMapper, opts ...Options) http.Handler { + o := &options{ + hostMapper: hostMapper, + onReqError: func(*http.Request, error) {}, + onResError: func(_ *http.Response, err error) error { return err }, + transport: http.DefaultTransport, + errHandler: defaultErrorHandler, + } + + for _, op := range opts { + op(o) + } + + rp := &httputil.ReverseProxy{ + Rewrite: rewriter(o), + ModifyResponse: modifyResponse(o), + Transport: o.transport, + ErrorHandler: o.errHandler, + } + + return o.beforeProxyMiddleware(rp) +} diff --git a/oryx/proxy/rewrites.go b/oryx/proxy/rewrites.go new file mode 100644 index 0000000..ddd6d1d --- /dev/null +++ b/oryx/proxy/rewrites.go @@ -0,0 +1,163 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package proxy + +import ( + "bytes" + "compress/gzip" + "io" + "net/http" + "net/url" + "path" + "strings" + + "github.com/pkg/errors" +) + +type compressableBody struct { + buf bytes.Buffer + w io.WriteCloser +} + +// we require a read and write for websocket connections +var _ io.ReadWriteCloser = new(compressableBody) + +func (b *compressableBody) Close() error { + if b != nil { + b.buf.Reset() + if b.w != nil { + return b.w.Close() + } + } + return nil +} + +func (b *compressableBody) Write(d []byte) (int, error) { + if b == nil { + // this happens when the body is empty + return 0, nil + } + + var w io.Writer = &b.buf + if b.w != nil { + w = b.w + defer b.w.Close() + } + return w.Write(d) +} + +func (b *compressableBody) Read(p []byte) (n int, err error) { + if b == nil { + // this happens when the body is empty + return 0, io.EOF + } + return b.buf.Read(p) +} + +func headerRequestRewrite(req *http.Request, c *HostConfig) { + req.URL.Scheme = c.UpstreamScheme + req.URL.Host = c.UpstreamHost + req.URL.Path = strings.TrimPrefix(req.URL.Path, c.PathPrefix) + + if _, ok := req.Header["User-Agent"]; !ok { + // explicitly disable User-Agent so it's not set to default value + req.Header.Set("User-Agent", "") + } +} + +func headerResponseRewrite(resp *http.Response, c *HostConfig) error { + redir, err := resp.Location() + if err != nil { + if !errors.Is(err, http.ErrNoLocation) { + return errors.WithStack(err) + } + } else if redir.Host == c.TargetHost { + redir.Scheme = c.originalScheme + redir.Host = c.originalHost + redir.Path = path.Join(c.PathPrefix, redir.Path) + resp.Header.Set("Location", redir.String()) + } + + ReplaceCookieDomainAndSecure(resp, c.TargetHost, c.CookieDomain, c.originalScheme == "https") + + return nil +} + +// ReplaceCookieDomainAndSecure replaces the domain of all matching Set-Cookie headers in the response. +func ReplaceCookieDomainAndSecure(resp *http.Response, original, replacement string, secure bool) { + original, replacement = stripPort(original), stripPort(replacement) // cookies don't distinguish ports + + cookies := resp.Cookies() + resp.Header.Del("Set-Cookie") + for _, co := range cookies { + co.Domain = replacement + co.Secure = secure + if !secure { + co.SameSite = http.SameSiteLaxMode + } + resp.Header.Add("Set-Cookie", co.String()) + } +} + +func bodyResponseRewrite(resp *http.Response, c *HostConfig) ([]byte, *compressableBody, error) { + if resp.ContentLength == 0 { + return nil, nil, nil + } + + body, cb, err := readBody(resp.Header, resp.Body) + if err != nil { + return nil, nil, err + } + + if c.TargetScheme == "" { + c.TargetScheme = "https" + } + + return bytes.ReplaceAll(body, []byte(c.TargetScheme+"://"+c.TargetHost), []byte(c.originalScheme+"://"+c.originalHost+c.PathPrefix)), cb, nil +} + +func readBody(h http.Header, body io.ReadCloser) ([]byte, *compressableBody, error) { + defer body.Close() + + cb := &compressableBody{} + + switch h.Get("Content-Encoding") { + case "gzip": + var err error + body, err = gzip.NewReader(body) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + cb.w = gzip.NewWriter(&cb.buf) + default: + // do nothing, we can read directly + } + + b, err := io.ReadAll(body) + if err != nil { + return nil, nil, errors.WithStack(err) + } + return b, cb, nil +} + +func handleWebsocketResponse(n int, cb *compressableBody, body io.ReadCloser) (int, io.ReadWriteCloser, error) { + var err error + readWriteCloser, ok := body.(io.ReadWriteCloser) + if ok { + if cb != nil { + n, err = readWriteCloser.Write(cb.buf.Bytes()) + if err != nil { + return 0, nil, errors.WithStack(err) + } + } + return n, readWriteCloser, nil + } + return n, cb, nil +} + +// stripPort removes the optional port from the host. +func stripPort(host string) string { + return (&url.URL{Host: host}).Hostname() +} diff --git a/oryx/proxy/stubs/auth.example.com.json b/oryx/proxy/stubs/auth.example.com.json new file mode 100644 index 0000000..2dcdc3b --- /dev/null +++ b/oryx/proxy/stubs/auth.example.com.json @@ -0,0 +1,9 @@ +{ + "ui": { + "action": "https://auth.example.com" + }, + "callbacks": [ + "https://auth.example.com/path/to/resource", + "https://auth.example.com/path?q=https://localhost:8000" + ] +} diff --git a/oryx/randx/README.md b/oryx/randx/README.md new file mode 100644 index 0000000..fb8c504 --- /dev/null +++ b/oryx/randx/README.md @@ -0,0 +1,10 @@ +`randx.RuneSequence` generates even distributions for the given character set +and length. All results are therefore also evenly distributed. + +## AlphaNum + +[Alphabet and Numeric](../docs/alpha_num.png) + +## AlphaNum + +[Alphabet and Numeric](../docs/num.png) diff --git a/oryx/randx/sequence.go b/oryx/randx/sequence.go new file mode 100644 index 0000000..f41b22e --- /dev/null +++ b/oryx/randx/sequence.go @@ -0,0 +1,75 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package randx + +import ( + "crypto/rand" + "math/big" + + "github.com/pkg/errors" +) + +var rander = rand.Reader // random function + +var ( + // AlphaNum contains runes [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]. + AlphaNum = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + // Alpha contains runes [abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]. + Alpha = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + // AlphaLowerNum contains runes [abcdefghijklmnopqrstuvwxyz0123456789]. + AlphaLowerNum = []rune("abcdefghijklmnopqrstuvwxyz0123456789") + // AlphaUpperNum contains runes [ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]. + AlphaUpperNum = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + // AlphaLower contains runes [abcdefghijklmnopqrstuvwxyz]. + AlphaLower = []rune("abcdefghijklmnopqrstuvwxyz") + // AlphaUpperVowels contains runes [AEIOUY]. + AlphaUpperVowels = []rune("AEIOUY") + // AlphaUpperNoVowels contains runes [BCDFGHJKLMNPQRSTVWXZ]. + AlphaUpperNoVowels = []rune("BCDFGHJKLMNPQRSTVWXZ") + // AlphaUpper contains runes [ABCDEFGHIJKLMNOPQRSTUVWXYZ]. + AlphaUpper = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + // Numeric contains runes [0123456789]. + Numeric = []rune("0123456789") + // AlphaNumNoAmbiguous is equivalent to AlphaNum but without visually ambiguous characters [0Oo1IlB8S5Z2]. + AlphaNumNoAmbiguous = []rune("abcdefghijkmnpqrstuvwxyzACDEFGHJKLMNPQRTUVWXY34679") +) + +// RuneSequence returns a random sequence using the defined allowed runes. +func RuneSequence(l int, allowedRunes []rune) (seq []rune, err error) { + c := big.NewInt(int64(len(allowedRunes))) + seq = make([]rune, l) + + for i := 0; i < l; i++ { + r, err := rand.Int(rander, c) + if err != nil { + return seq, err + } + rn := allowedRunes[r.Uint64()] + seq[i] = rn + } + + return seq, nil +} + +// MustString returns a random string sequence using the defined runes. Panics on error. +func MustString(l int, allowedRunes []rune) string { + seq, err := RuneSequence(l, allowedRunes) + if err != nil { + panic(err) + } + return string(seq) +} + +func GenerateNonce() ([]byte, error) { + const challengeSize = 32 + + challenge := make([]byte, challengeSize) + + _, err := rand.Read(challenge) + if err != nil { + return nil, errors.WithStack(err) + } + + return challenge, nil +} diff --git a/oryx/randx/strength/go.mod b/oryx/randx/strength/go.mod new file mode 100644 index 0000000..968295e --- /dev/null +++ b/oryx/randx/strength/go.mod @@ -0,0 +1,22 @@ +module github.com/ory/x/randx/strength + +go 1.26 + +replace github.com/ory/x => ../.. + +require ( + github.com/ory/x v0.0.0-00010101000000-000000000000 + gonum.org/v1/plot v0.17.0 +) + +require ( + codeberg.org/go-fonts/liberation v0.5.0 // indirect + codeberg.org/go-latex/latex v0.2.0 // indirect + codeberg.org/go-pdf/fpdf v0.11.1 // indirect + git.sr.ht/~sbinet/gg v0.7.0 // indirect + github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/image v0.41.0 // indirect + golang.org/x/text v0.39.0 // indirect +) diff --git a/oryx/randx/strength/go.sum b/oryx/randx/strength/go.sum new file mode 100644 index 0000000..811ddce --- /dev/null +++ b/oryx/randx/strength/go.sum @@ -0,0 +1,65 @@ +codeberg.org/go-fonts/dejavu v0.4.0 h1:2yn58Vkh4CFK3ipacWUAIE3XVBGNa0y1bc95Bmfx91I= +codeberg.org/go-fonts/dejavu v0.4.0/go.mod h1:abni088lmhQJvso2Lsb7azCKzwkfcnttl6tL1UTWKzg= +codeberg.org/go-fonts/latin-modern v0.4.0 h1:vkRCc1y3whKA7iL9Ep0fSGVuJfqjix0ica9UflHORO8= +codeberg.org/go-fonts/latin-modern v0.4.0/go.mod h1:BF68mZznJ9QHn+hic9ks2DaFl4sR5YhfM6xTYaP9vNw= +codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0= +codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU= +codeberg.org/go-latex/latex v0.2.0 h1:Ol/a6VHY06N+5gPfewswymoRb5ZcKDXWVaVegcx4hbI= +codeberg.org/go-latex/latex v0.2.0/go.mod h1:VJAwQir7/T8LZxj7xAPivISKiVOwkMpQ8bTuPQ31X0Y= +codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs= +codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU= +git.sr.ht/~sbinet/cmpimg v0.1.0 h1:E0zPRk2muWuCqSKSVZIWsgtU9pjsw3eKHi8VmQeScxo= +git.sr.ht/~sbinet/cmpimg v0.1.0/go.mod h1:FU12psLbF4TfNXkKH2ZZQ29crIqoiqTZmeQ7dkp/pxE= +git.sr.ht/~sbinet/gg v0.7.0 h1:YmNf7YKd7diDMTPm86hZa1EM3pbkOyD/zzjl0LZUdNM= +git.sr.ht/~sbinet/gg v0.7.0/go.mod h1:VYeli15tpMM4EvqlivlVbbyvWZlOU+EZn4XZmfBGUdM= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.41.0 h1:8wS72eGJMJaBxK6okTzd4WaXumUlTVlb753MlsSvTCo= +golang.org/x/image v0.41.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= +golang.org/x/mod v0.3.0/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +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/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +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.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/plot v0.17.0 h1:d0DwPVBe9jnEGqQBoZGl/P2M9WciJbG2CnV59C9QBT4= +gonum.org/v1/plot v0.17.0/go.mod h1:ipt2GUN1oqzr2O7wCjLDtw1ShfIYYNBp4o0O1Ez5B3Y= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/oryx/randx/strength/main.go b/oryx/randx/strength/main.go new file mode 100644 index 0000000..26c877c --- /dev/null +++ b/oryx/randx/strength/main.go @@ -0,0 +1,101 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "sort" + + "gonum.org/v1/plot" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/plotutil" + "gonum.org/v1/plot/vg" + + "github.com/ory/x/randx" +) + +const iterations = 1000 * 100 + +type generate func(int, []rune) ([]rune, error) + +func main() { + draw(measureDistribution(iterations, randx.AlphaNum, randx.RuneSequence), "AlphaNum Distribution", "docs/alpha_num.png") + draw(measureDistribution(iterations, randx.Numeric, randx.RuneSequence), "Num Distribution", "docs/num.png") + draw(measureResultDistribution(100, 6, randx.Numeric, randx.RuneSequence), "Num Distribution", "docs/result_num.png") +} + +func measureResultDistribution(iterations int, length int, characters []rune, fn generate) map[string]int { + dist := make(map[string]int) + for index := 1; index <= iterations; index++ { + // status output to cli + if index%1000 == 0 { + fmt.Printf("\r%d / %d", index, iterations) + } + raw, err := fn(length, characters) + if err != nil { + panic(err) + } + dist[string(raw)] = dist[string(raw)] + 1 + } + return dist +} + +func measureDistribution(iterations int, characters []rune, fn generate) map[string]int { + dist := make(map[string]int) + for index := 1; index <= iterations; index++ { + // status output to cli + if index%1000 == 0 { + fmt.Printf("\r%d / %d", index, iterations) + } + raw, err := fn(100, characters) + if err != nil { + panic(err) + } + for _, s := range raw { + c := string(s) + i := dist[c] + dist[c] = i + 1 + } + } + return dist +} + +func draw(distribution map[string]int, title, filename string) { + keys, values := orderMap(distribution) + group := plotter.Values{} + for _, v := range values { + group = append(group, float64(v)) + } + + p := plot.New() + p.Title.Text = title + p.Y.Label.Text = "N" + + bars, err := plotter.NewBarChart(group, vg.Points(4)) + if err != nil { + panic(err) + } + bars.LineStyle.Width = vg.Length(0) + bars.Color = plotutil.Color(0) + + p.Add(bars) + p.NominalX(keys...) + + if err := p.Save(300*vg.Millimeter, 150*vg.Millimeter, filename); err != nil { + panic(err) + } +} + +func orderMap(m map[string]int) (keys []string, values []int) { + keys = []string{} + values = []int{} + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + values = append(values, m[key]) + } + return keys, values +} diff --git a/oryx/region/region.go b/oryx/region/region.go new file mode 100644 index 0000000..dd7dc13 --- /dev/null +++ b/oryx/region/region.go @@ -0,0 +1,142 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package region defines the Ory Network region enumeration. Lives in x/ so +// kratos-oss and other OSS layers can use it without importing cloudlib. +package region + +import ( + "database/sql/driver" + + "github.com/pkg/errors" + + "github.com/ory/herodot" +) + +// Region is an Ory Network region. Specific regions map to a single CRDB +// region; super-regions (EU, Asia, US, Global) group specific regions that +// share a data-residency boundary. +// +// swagger:enum Region +type Region string + +const ( + EUCentral Region = "eu-central" + AsiaNorthEast Region = "asia-northeast" + USEast Region = "us-east" + USWest Region = "us-west" + + EU Region = "eu" + Asia Region = "asia" + US Region = "us" + Global Region = "global" +) + +// All returns every valid region in stable order: specific regions first, +// then super-regions. +func All() []Region { + return []Region{EUCentral, AsiaNorthEast, USEast, USWest, EU, Asia, US, Global} +} + +func (r Region) String() string { + return string(r) +} + +// Valid reports whether r is a known region. +func (r Region) Valid() bool { + switch r { + case EUCentral, AsiaNorthEast, USEast, USWest, EU, Asia, US, Global: + return true + } + return false +} + +// IsSuperRegion reports whether r is a super-region. +func (r Region) IsSuperRegion() bool { + switch r { + case EU, Asia, US, Global: + return true + } + return false +} + +// Contains reports whether r contains other: +// - Global contains every valid region. +// - EU contains EUCentral; Asia contains AsiaNorthEast; US contains USEast and USWest. +// - Every region contains itself. +func (r Region) Contains(other Region) bool { + if !r.Valid() || !other.Valid() { + return false + } + if r == Global || r == other { + return true + } + switch r { + case EU: + return other == EUCentral + case Asia: + return other == AsiaNorthEast + case US: + return other == USEast || other == USWest + } + return false +} + +// Scan implements sql.Scanner. NULL and empty scan to the zero value; +// validate via Region.Valid if "unset" must be rejected. +func (r *Region) Scan(src any) error { + switch s := src.(type) { + case nil: + *r = "" + case string: + *r = Region(s) + case []byte: + *r = Region(s) + default: + return errors.Errorf("cannot scan %T into region.Region", src) + } + return nil +} + +// Value implements driver.Valuer. The empty Region writes as "". +func (r Region) Value() (driver.Value, error) { + return string(r), nil +} + +// IsEqual compares two nullable *Region pointers (both nil = equal). +func IsEqual(a, b *Region) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return *a == *b +} + +// ErrInvalid is wrapped when a submitted region is not a known value. +// Use NewErrInvalid for a herodot 400; use errors.Is for chain checks. +var ErrInvalid = errors.New("the provided region is not a valid Ory region") + +// ErrNotAllowed is wrapped when a valid region is outside the project's +// home_region constraint. +var ErrNotAllowed = errors.New("the provided region is not allowed by this project's home region") + +// NewErrInvalid returns a fresh herodot 400 wrapping ErrInvalid. +func NewErrInvalid() error { + return errors.WithStack( + herodot.ErrBadRequest(). + WithReason(ErrInvalid.Error()). + WithDebug(`region must be one of eu-central, asia-northeast, us-east, us-west, eu, asia, us, global`). + WithWrap(ErrInvalid), + ) +} + +// NewErrNotAllowed returns a fresh herodot 400 wrapping ErrNotAllowed. +func NewErrNotAllowed() error { + return errors.WithStack( + herodot.ErrBadRequest(). + WithReason(ErrNotAllowed.Error()). + WithWrap(ErrNotAllowed), + ) +} diff --git a/oryx/reqlog/LICENSE b/oryx/reqlog/LICENSE new file mode 100644 index 0000000..638544b --- /dev/null +++ b/oryx/reqlog/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Dan Buch and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/oryx/reqlog/external_latency.go b/oryx/reqlog/external_latency.go new file mode 100644 index 0000000..53edda9 --- /dev/null +++ b/oryx/reqlog/external_latency.go @@ -0,0 +1,34 @@ +// Copyright © 2024 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package reqlog + +import ( + "context" + "sync/atomic" + "time" +) + +func withEnableExternalLatencyMeasurement(ctx context.Context) context.Context { + return context.WithValue(ctx, externalLatencyKey, new(int64)) +} + +func AccumulateExternalLatency(ctx context.Context, dur time.Duration) { + total, ok := ctx.Value(externalLatencyKey).(*int64) + if !ok { + return + } + atomic.AddInt64(total, int64(dur)) +} + +func getExternalLatency(ctx context.Context) time.Duration { + total, ok := ctx.Value(externalLatencyKey).(*int64) + if !ok { + return 0 + } + return time.Duration(atomic.LoadInt64(total)) +} + +type contextKey int + +const externalLatencyKey contextKey = 1 diff --git a/oryx/reqlog/middleware.go b/oryx/reqlog/middleware.go new file mode 100644 index 0000000..59dc281 --- /dev/null +++ b/oryx/reqlog/middleware.go @@ -0,0 +1,191 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package reqlog + +import ( + "net/http" + "sync" + "time" + + "github.com/sirupsen/logrus" + "github.com/urfave/negroni" + + "github.com/ory/x/logrusx" +) + +type timer interface { + Now() time.Time + Since(time.Time) time.Duration +} + +type realClock struct{} + +func (rc *realClock) Now() time.Time { + return time.Now() +} + +func (rc *realClock) Since(t time.Time) time.Duration { + return time.Since(t) +} + +// Middleware is a middleware handler that logs the request as it goes in and the response as it goes out. +type Middleware struct { + // Logger is the log.Logger instance used to log messages with the Logger middleware + Logger *logrusx.Logger + // Name is the name of the application as recorded in latency metrics + Name string + Before func(*logrusx.Logger, *http.Request, string) *logrusx.Logger + After func(*logrusx.Logger, *http.Request, negroni.ResponseWriter, time.Duration, string) *logrusx.Logger + + logStarting bool + + clock timer + + logLevel logrus.Level + + // Silence log for specific URL paths + silencePaths map[string]bool + + sync.RWMutex +} + +// NewMiddleware returns a reqlog middleware with default settings +func NewMiddleware() *Middleware { + return NewCustomMiddleware(logrus.InfoLevel, &logrus.TextFormatter{}, "web") +} + +// NewCustomMiddleware returns a reqlog middleware with the given level and formatter +func NewCustomMiddleware(level logrus.Level, formatter logrus.Formatter, name string) *Middleware { + log := logrusx.New(name, "", logrusx.ForceFormatter(formatter), logrusx.ForceLevel(level)) + return &Middleware{ + Logger: log, + Name: name, + Before: DefaultBefore, + After: DefaultAfter, + + logLevel: logrus.InfoLevel, + logStarting: true, + clock: &realClock{}, + silencePaths: map[string]bool{}, + } +} + +// NewMiddlewareFromLogger returns a reqlog middleware which writes to a given logrus logger. +func NewMiddlewareFromLogger(logger *logrusx.Logger, name string) *Middleware { + return &Middleware{ + Logger: logger, + Name: name, + Before: DefaultBefore, + After: DefaultAfter, + + logLevel: logrus.InfoLevel, + logStarting: true, + clock: &realClock{}, + silencePaths: map[string]bool{}, + } +} + +// SetLogStarting accepts a bool to control the logging of "started handling +// request" prior to passing to the next middleware +func (m *Middleware) SetLogStarting(v bool) { + m.logStarting = v +} + +// ExcludePaths adds new URL paths to be ignored during logging. The URL u is parsed, hence the returned error +func (m *Middleware) ExcludePaths(paths ...string) *Middleware { + for _, path := range paths { + m.Lock() + m.silencePaths[path] = true + m.Unlock() + } + return m +} + +func (m *Middleware) Wrap(handler http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + m.ServeHTTP(rw, r, handler.ServeHTTP) + }) +} + +func (m *Middleware) WrapFunc(handler http.HandlerFunc) http.HandlerFunc { + return func(rw http.ResponseWriter, r *http.Request) { + m.ServeHTTP(rw, r, handler) + } +} + +func (m *Middleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if m.Before == nil { + m.Before = DefaultBefore + } + + if m.After == nil { + m.After = DefaultAfter + } + + logLevel := m.logLevel + m.RLock() + if _, ok := m.silencePaths[r.URL.Path]; ok { + logLevel = logrus.TraceLevel + } + m.RUnlock() + + start := m.clock.Now() + + // Try to get the real IP + remoteAddr := r.RemoteAddr + if realIP := r.Header.Get("X-Real-IP"); realIP != "" { + remoteAddr = realIP + } + + entry := m.Logger.NewEntry() + + entry = m.Before(entry, r, remoteAddr) + + if m.logStarting { + entry.Log(logLevel, "started handling request") + } + + nrw, ok := rw.(negroni.ResponseWriter) + if !ok { + nrw = negroni.NewResponseWriter(rw) + } + + r = r.WithContext(withEnableExternalLatencyMeasurement(r.Context())) + next(nrw, r) + + latency := m.clock.Since(start) + + m.After(entry, r, nrw, latency, m.Name).Log(logLevel, "completed handling request") +} + +// BeforeFunc is the func type used to modify or replace the *logrusx.Logger prior +// to calling the next func in the middleware chain +type BeforeFunc func(*logrusx.Logger, *http.Request, string) *logrusx.Logger + +// AfterFunc is the func type used to modify or replace the *logrusx.Logger after +// calling the next func in the middleware chain +type AfterFunc func(*logrusx.Logger, negroni.ResponseWriter, time.Duration, string) *logrusx.Logger + +// DefaultBefore is the default func assigned to *Middleware.Before +func DefaultBefore(entry *logrusx.Logger, req *http.Request, remoteAddr string) *logrusx.Logger { + return entry.WithRequest(req) +} + +// DefaultAfter is the default func assigned to *Middleware.After +func DefaultAfter(entry *logrusx.Logger, req *http.Request, res negroni.ResponseWriter, latency time.Duration, name string) *logrusx.Logger { + e := entry.WithRequest(req).WithField("http_response", map[string]any{ + "status": res.Status(), + "size": res.Size(), + "text_status": http.StatusText(res.Status()), + "took": latency, + "headers": entry.HTTPHeadersRedacted(res.Header()), + }) + if el := getExternalLatency(req.Context()); el > 0 { + e = e.WithFields(map[string]any{ + "took_internal": latency - el, + "took_external": el, + }) + } + return e +} diff --git a/oryx/resilience/retry.go b/oryx/resilience/retry.go new file mode 100644 index 0000000..8ca6511 --- /dev/null +++ b/oryx/resilience/retry.go @@ -0,0 +1,39 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package resilience provides helpers for dealing with resilience. +package resilience + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/ory/x/logrusx" +) + +// Retry executes a f until no error is returned or failAfter is reached. +func Retry(logger *logrusx.Logger, maxWait time.Duration, failAfter time.Duration, f func() error) (err error) { + var lastStart time.Time + err = errors.New("did not connect") + loopWait := time.Millisecond * 100 + retryStart := time.Now().UTC() + for retryStart.Add(failAfter).After(time.Now().UTC()) { + lastStart = time.Now().UTC() + if err = f(); err == nil { + return nil + } + + if lastStart.Add(maxWait * 2).Before(time.Now().UTC()) { + retryStart = time.Now().UTC() + } + + logger.WithError(err).Infof("Retrying in %f seconds...", loopWait.Seconds()) + time.Sleep(loopWait) + loopWait = loopWait * time.Duration(int64(2)) + if loopWait > maxWait { + loopWait = maxWait + } + } + return err +} diff --git a/oryx/safecast/safecast.go b/oryx/safecast/safecast.go new file mode 100644 index 0000000..947b517 --- /dev/null +++ b/oryx/safecast/safecast.go @@ -0,0 +1,14 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package safecast + +import "math" + +// Clamp if needed. +func Uint64ToInt64(in uint64) int64 { + if in > math.MaxInt64 { + return math.MaxInt64 + } + return int64(in) +} diff --git a/oryx/serverx/404.go b/oryx/serverx/404.go new file mode 100644 index 0000000..5e5a68b --- /dev/null +++ b/oryx/serverx/404.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package serverx + +import ( + _ "embed" + "net/http" + + "github.com/ory/herodot/httputil" +) + +//go:embed 404.html +var page404HTML string + +//go:embed 404.json +var page404JSON string + +// DefaultNotFoundHandler is a default handler for handling 404 errors. +var DefaultNotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var contentType, body string + switch httputil.NegotiateContentType(r, []string{ + "text/html", + "text/plain", + "application/json", + }, "text/html") { + case "text/plain": + contentType = "text/plain" + body = "Error 404 - The requested route does not exist. Make sure you are using the right path, domain, and port." + case "application/json": + contentType = "application/json" + body = page404JSON + default: + fallthrough + case "text/html": + contentType = "text/html" + body = page404HTML + } + + w.Header().Set("Content-Type", contentType+"; charset=utf-8") + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte(body)) +}) diff --git a/oryx/serverx/404.html b/oryx/serverx/404.html new file mode 100644 index 0000000..d2031d1 --- /dev/null +++ b/oryx/serverx/404.html @@ -0,0 +1,56 @@ + + + + + 404 - Route not found + + + +
+
+

Error 404

+

+ The requested route does not exist. Make sure you are using the right + path, domain, and port. +

+
+
+ + diff --git a/oryx/serverx/404.json b/oryx/serverx/404.json new file mode 100644 index 0000000..5f46c1c --- /dev/null +++ b/oryx/serverx/404.json @@ -0,0 +1,7 @@ +{ + "error": { + "code": 404, + "message": "Not Found", + "reason": "The requested route does not exist. Make sure you are using the right path, domain, and port." + } +} diff --git a/oryx/servicelocatorx/options.go b/oryx/servicelocatorx/options.go new file mode 100644 index 0000000..9609bbc --- /dev/null +++ b/oryx/servicelocatorx/options.go @@ -0,0 +1,83 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package servicelocatorx + +import ( + "github.com/urfave/negroni" + "google.golang.org/grpc" + + "github.com/ory/x/contextx" + "github.com/ory/x/logrusx" +) + +type ( + Options struct { + logger *logrusx.Logger + contextualizer contextx.Contextualizer + httpMiddlewares []negroni.Handler + grpcUnaryInterceptors []grpc.UnaryServerInterceptor + grpcStreamInterceptors []grpc.StreamServerInterceptor + } + Option func(o *Options) +) + +func WithLogger(l *logrusx.Logger) Option { + return func(o *Options) { + o.logger = l + } +} + +func WithContextualizer(ctxer contextx.Contextualizer) Option { + return func(o *Options) { + o.contextualizer = ctxer + } +} + +func WithHTTPMiddlewares(m ...negroni.Handler) Option { + return func(o *Options) { + o.httpMiddlewares = m + } +} + +func WithGRPCUnaryInterceptors(i ...grpc.UnaryServerInterceptor) Option { + return func(o *Options) { + o.grpcUnaryInterceptors = i + } +} + +func WithGRPCStreamInterceptors(i ...grpc.StreamServerInterceptor) Option { + return func(o *Options) { + o.grpcStreamInterceptors = i + } +} + +func (o *Options) Logger() *logrusx.Logger { + return o.logger +} + +func (o *Options) Contextualizer() contextx.Contextualizer { + return o.contextualizer +} + +func (o *Options) HTTPMiddlewares() []negroni.Handler { + return o.httpMiddlewares +} + +func (o *Options) GRPCUnaryInterceptors() []grpc.UnaryServerInterceptor { + return o.grpcUnaryInterceptors +} + +func (o *Options) GRPCStreamInterceptors() []grpc.StreamServerInterceptor { + return o.grpcStreamInterceptors +} + +func NewOptions(options ...Option) *Options { + o := &Options{ + contextualizer: &contextx.Default{}, + } + for _, opt := range options { + opt(o) + } + return o +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json new file mode 100644 index 0000000..4bc224c --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=1.json-fn.json @@ -0,0 +1,27 @@ +{ + "foo": { + "other": "fdsa" + }, + "nested": { + "nested": { + "arr": [ + { + }, + { + } + ] + } + }, + "arr": [ + { + }, + { + "arr": [ + { + }, + { + } + ] + } + ] +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json new file mode 100644 index 0000000..9926439 --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=2.json-fn.json @@ -0,0 +1,34 @@ +{ + "created_at": "1234", + "updated_at": "1234", + "nested": { + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] +} diff --git a/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json new file mode 100644 index 0000000..5e9b1c9 --- /dev/null +++ b/oryx/snapshotx/.snapshots/TestDeleteMatches-file=3.json-fn.json @@ -0,0 +1,28 @@ +{ + "updated_at": "1234", + "nested": { + "nested": { + "arr": [ + { + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + }, + { + "updated_at": 1234, + "arr": [ + { + }, + { + "updated_at": 1234 + } + ] + } + ] +} diff --git a/oryx/snapshotx/fixtures/1.json b/oryx/snapshotx/fixtures/1.json new file mode 100644 index 0000000..a0d0535 --- /dev/null +++ b/oryx/snapshotx/fixtures/1.json @@ -0,0 +1,47 @@ +{ + "ignore_nested": [ + "updated_at", + "created_at" + ], + "ignore_exact": [ + "foo.id" + ], + "content": { + "foo": { + "id": "asdf", + "other": "fdsa" + }, + "created_at": "1234", + "updated_at": "1234", + "nested":{ + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/fixtures/2.json b/oryx/snapshotx/fixtures/2.json new file mode 100644 index 0000000..dbe84c0 --- /dev/null +++ b/oryx/snapshotx/fixtures/2.json @@ -0,0 +1,38 @@ +{ + "ignore_nested": [ + ], + "content": { + "created_at": "1234", + "updated_at": "1234", + "nested":{ + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/fixtures/3.json b/oryx/snapshotx/fixtures/3.json new file mode 100644 index 0000000..bc58d3f --- /dev/null +++ b/oryx/snapshotx/fixtures/3.json @@ -0,0 +1,39 @@ +{ + "ignore_nested": [ + "created_at" + ], + "content": { + "created_at": "1234", + "updated_at": "1234", + "nested": { + "created_at": 1234, + "nested": { + "created_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + }, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234, + "arr": [ + { + "created_at": 1234 + }, + { + "updated_at": 1234 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/oryx/snapshotx/snapshot.go b/oryx/snapshotx/snapshot.go new file mode 100644 index 0000000..e8b0b6d --- /dev/null +++ b/oryx/snapshotx/snapshot.go @@ -0,0 +1,144 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package snapshotx + +import ( + "bytes" + "encoding/json" + "fmt" + "slices" + "strings" + "testing" + + "github.com/bradleyjkemp/cupaloy/v2" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +type ( + Opt = func(*options) + options struct { + modifiers []func(t *testing.T, raw []byte) []byte + name string + } +) + +func ExceptPaths(keys ...string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + for _, key := range keys { + var err error + raw, err = sjson.DeleteBytes(raw, key) + require.NoError(t, err) + } + return raw + }) + } +} + +func ExceptNestedKeys(nestedKeys ...string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + parsed := gjson.ParseBytes(raw) + require.True(t, parsed.IsObject() || parsed.IsArray()) + return deleteMatches(t, "", parsed, nestedKeys, []string{}, raw) + }) + } +} + +func WithReplacement(str, replace string) Opt { + return func(o *options) { + o.modifiers = append(o.modifiers, func(t *testing.T, raw []byte) []byte { + return bytes.ReplaceAll(raw, []byte(str), []byte(replace)) + }) + } +} + +func WithName(name string) Opt { + return func(o *options) { + o.name = name + } +} + +func newOptions(opts ...Opt) *options { + o := &options{} + for _, opt := range opts { + opt(o) + } + return o +} + +func (o *options) applyModifiers(t *testing.T, compare []byte) []byte { + for _, modifier := range o.modifiers { + compare = modifier(t, compare) + } + return compare +} + +var snapshot = cupaloy.New(cupaloy.SnapshotFileExtension(".json")) + +func SnapshotTJSON[C ~string | ~[]byte](t *testing.T, compare C, opts ...Opt) { + SnapshotT(t, json.RawMessage(compare), opts...) +} + +func SnapshotT(t *testing.T, actual any, opts ...Opt) { + t.Helper() + compare, err := json.MarshalIndent(actual, "", " ") + require.NoErrorf(t, err, "%+v", actual) + + o := newOptions(opts...) + compare = o.applyModifiers(t, compare) + + if o.name == "" { + snapshot.SnapshotT(t, compare) + } else { + name := strings.ReplaceAll(t.Name()+"_"+o.name, "/", "-") + require.NoError(t, snapshot.SnapshotWithName(name, compare)) + } +} + +// SnapshotTExcept is deprecated in favor of SnapshotT with Opt. +// +// DEPRECATED: please use SnapshotT instead +func SnapshotTExcept(t *testing.T, actual interface{}, except []string) { + t.Helper() + compare, err := json.MarshalIndent(actual, "", " ") + require.NoError(t, err, "%+v", actual) + for _, e := range except { + compare, err = sjson.DeleteBytes(compare, e) + require.NoError(t, err, "%s", e) + } + + snapshot.SnapshotT(t, compare) +} + +func deleteMatches(t *testing.T, key string, result gjson.Result, matches []string, parents []string, content []byte) []byte { + path := parents + if key != "" { + path = append(parents, key) + } + + if result.IsObject() { + result.ForEach(func(key, value gjson.Result) bool { + content = deleteMatches(t, key.String(), value, matches, path, content) + return true + }) + } else if result.IsArray() { + var i int + result.ForEach(func(_, value gjson.Result) bool { + content = deleteMatches(t, fmt.Sprintf("%d", i), value, matches, path, content) + i++ + return true + }) + } + + if slices.Contains(matches, key) { + content, err := sjson.DeleteBytes(content, strings.Join(path, ".")) + require.NoError(t, err) + return content + } + + return content +} diff --git a/oryx/sqlcon/connector.go b/oryx/sqlcon/connector.go new file mode 100644 index 0000000..a21539c --- /dev/null +++ b/oryx/sqlcon/connector.go @@ -0,0 +1,23 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package sqlcon provides helpers for dealing with SQL connectivity. +package sqlcon + +import ( + "runtime" + "strings" +) + +// GetDriverName returns the driver name of a given DSN. +func GetDriverName(dsn string) string { + return strings.Split(dsn, "://")[0] +} +func maxParallelism() int { + maxProcs := runtime.GOMAXPROCS(0) + numCPU := runtime.NumCPU() + if maxProcs < numCPU { + return maxProcs + } + return numCPU +} diff --git a/oryx/sqlcon/dockertest/cockroach.go b/oryx/sqlcon/dockertest/cockroach.go new file mode 100644 index 0000000..c2d1820 --- /dev/null +++ b/oryx/sqlcon/dockertest/cockroach.go @@ -0,0 +1,22 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dockertest + +import ( + "testing" + + "github.com/cockroachdb/cockroach-go/v2/testserver" + "github.com/stretchr/testify/require" +) + +func NewLocalTestCRDBServer(t testing.TB) string { + ts, err := testserver.NewTestServer(testserver.CustomVersionOpt("25.3.3")) + require.NoError(t, err) + t.Cleanup(ts.Stop) + + require.NoError(t, ts.WaitForInit()) + + ts.PGURL().Scheme = "cockroach" + return ts.PGURL().String() +} diff --git a/oryx/sqlcon/dockertest/test_helper.go b/oryx/sqlcon/dockertest/test_helper.go new file mode 100644 index 0000000..4b0c237 --- /dev/null +++ b/oryx/sqlcon/dockertest/test_helper.go @@ -0,0 +1,195 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package dockertest + +import ( + "cmp" + "fmt" + "io" + "os" + "regexp" + "strings" + "testing" + "time" + + "github.com/moby/moby/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/pop/v6" + + "github.com/ory/dockertest/v4" + "github.com/ory/x/stringsx" +) + +func ConnectPop(t require.TestingT, url string) (c *pop.Connection) { + require.EventuallyWithT(t, func(t *assert.CollectT) { + var err error + c, err = pop.NewConnection(&pop.ConnectionDetails{ + URL: url, + }) + require.NoError(t, err) + require.NoError(t, c.Open()) + require.NoError(t, c.RawQuery("select version()").Exec()) + }, 5*time.Minute, time.Second*5, "could not connect to database at %s", url) + return +} + +// ## PostgreSQL ## + +func startPostgreSQL(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "postgres", + dockertest.WithTag(cmp.Or(version, "16")), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{"PGUSER=postgres", "POSTGRES_PASSWORD=secret", "POSTGRES_DB=postgres"}), + ) +} + +// RunTestPostgreSQL runs a PostgreSQL database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestPostgreSQL(t testing.TB) string { + if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_POSTGRESQL is set to: %s", dsn) + return dsn + } + r := startPostgreSQL(t, "") + return fmt.Sprintf("postgres://postgres:secret@127.0.0.1:%s/postgres?sslmode=disable", r.GetPort("5432/tcp")) +} + +// RunTestPostgreSQLWithVersion connects to a PostgreSQL database. +func RunTestPostgreSQLWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_POSTGRESQL"); dsn != "" { + return dsn + } + r := startPostgreSQL(t, version) + return fmt.Sprintf("postgres://postgres:secret@127.0.0.1:%s/postgres?sslmode=disable", r.GetPort("5432/tcp")) +} + +// ## MySQL ## + +func startMySQL(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "mysql", + dockertest.WithTag(cmp.Or(version, "8.0")), + dockertest.WithoutReuse(), + dockertest.WithEnv([]string{ + "MYSQL_ROOT_PASSWORD=secret", + "MYSQL_ROOT_HOST=%", + }), + ) +} + +// RunTestMySQL runs a MySQL database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestMySQL(t testing.TB) string { + if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_MYSQL is set to: %s", dsn) + return dsn + } + r := startMySQL(t, "") + return fmt.Sprintf("mysql://root:secret@tcp(localhost:%s)/mysql?parseTime=true&multiStatements=true", r.GetPort("3306/tcp")) +} + +// RunTestMySQLWithVersion runs a MySQL database in the specified version and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestMySQLWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_MYSQL"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_MYSQL is set to: %s", dsn) + return dsn + } + r := startMySQL(t, version) + return fmt.Sprintf("mysql://root:secret@tcp(localhost:%s)/mysql?parseTime=true&multiStatements=true", r.GetPort("3306/tcp")) +} + +// ## CockroachDB + +func startCockroachDB(t testing.TB, version string) dockertest.Resource { + pool := dockertest.NewPoolT(t, "") + return pool.RunT(t, "cockroachdb/cockroach", + dockertest.WithTag(cmp.Or(version, "latest-v25.4")), + dockertest.WithoutReuse(), + dockertest.WithCmd([]string{"start-single-node", "--insecure"}), + ) +} + +// RunTestCockroachDB runs a CockroachDB database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestCockroachDB(t testing.TB) string { + return RunTestCockroachDBWithVersion(t, "") +} + +// RunTestCockroachDBWithVersion runs a CockroachDB database and returns the URL to it. +// If a docker container is started for the database, the container be removed +// at the end of the test. +func RunTestCockroachDBWithVersion(t testing.TB, version string) string { + if dsn := os.Getenv("TEST_DATABASE_COCKROACHDB"); dsn != "" { + t.Logf("Skipping Docker setup because environment variable TEST_DATABASE_COCKROACHDB is set to: %s", dsn) + return dsn + } + r := startCockroachDB(t, version) + return fmt.Sprintf("cockroach://root@localhost:%s/defaultdb?sslmode=disable", r.GetPort("26257/tcp")) +} + +func DumpSchema(t testing.TB, c *pop.Connection) string { + name, database, port := c.Dialect.Name(), c.Dialect.Details().Database, c.Dialect.Details().Port + t.Logf("Dumping schema for dialect %s, database %s on port %s", name, database, port) + + var cmd []string + var appendToDump string + switch dialects := stringsx.SwitchExact(name); { + case dialects.AddCase("sqlite3"): + return dumpSQLiteSchema(t, c) + case dialects.AddCase("postgres"): + cmd = []string{"pg_dump", "--username", "postgres", "--schema-only", "--dbname", database} + // we need to set the search path because the postgres dump always unsets it + appendToDump = "SET search_path TO public;\n" + case dialects.AddCase("mysql"): + cmd = []string{"mysqldump", "--user", "root", "--password=secret", "--no-data", database} + case dialects.AddCase("cockroach"): + cmd = []string{"cockroach", "sql", "--insecure", "--database", database, "--execute", "SHOW CREATE ALL TABLES; SHOW CREATE ALL TYPES;", "--format", "raw"} + default: + t.Log(dialects.ToUnknownCaseErr()) + t.FailNow() + return "" + } + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + require.NoError(t, err) + list, err := cli.ContainerList(t.Context(), client.ContainerListOptions{ + Filters: client.Filters{}.Add("publish", port), + }) + require.NoError(t, err) + require.Lenf(t, list.Items, 1, "expected exactly one %s container with port %s", name, port) + + process, err := cli.ExecCreate(t.Context(), list.Items[0].ID, client.ExecCreateOptions{ + TTY: true, + AttachStdout: true, + Cmd: cmd, + }) + require.NoError(t, err) + + resp, err := cli.ExecAttach(t.Context(), process.ID, client.ExecAttachOptions{ + TTY: true, + }) + require.NoError(t, err) + dump, err := io.ReadAll(resp.Reader) + require.NoErrorf(t, err, "%s", dump) + + d := string(dump) + appendToDump + d = regexp.MustCompile(`(--|#|\\|mysqldump|SHOW CREATE)[^\n]*\n`).ReplaceAllLiteralString(d, "") // comments and other non-schema lines + d = strings.ReplaceAll(d, "\r\n", "\n") + d = regexp.MustCompile(`\n\n+`).ReplaceAllLiteralString(d, "\n\n") + return d +} + +func dumpSQLiteSchema(t testing.TB, c *pop.Connection) string { + var sqls []string + require.NoError(t, c.RawQuery("SELECT sql FROM sqlite_master WHERE type IN ('table', 'index', 'trigger', 'view') AND name NOT LIKE 'sqlite_%' ORDER BY name").All(&sqls)) + return strings.Join(sqls, ";\n") + ";\n" +} diff --git a/oryx/sqlcon/error.go b/oryx/sqlcon/error.go new file mode 100644 index 0000000..d7c913e --- /dev/null +++ b/oryx/sqlcon/error.go @@ -0,0 +1,150 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "database/sql" + stderrs "errors" + "net/http" + + "github.com/go-sql-driver/mysql" + "github.com/jackc/pgx/v5/pgconn" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "modernc.org/sqlite" + + "github.com/ory/herodot" +) + +// ErrUniqueViolation is returned when a SQL INSERT / UPDATE command returns a conflict. +func ErrUniqueViolation() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusConflict, + GRPCCodeField: codes.AlreadyExists, + StatusField: http.StatusText(http.StatusConflict), + ErrorField: "Unable to insert or update resource because a resource with that value exists already", + } +} + +// ErrNoRows is returned when a SQL SELECT statement returns no rows. +func ErrNoRows() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusNotFound, + GRPCCodeField: codes.NotFound, + StatusField: http.StatusText(http.StatusNotFound), + ErrorField: "Unable to locate the resource", + } +} + +// ErrConcurrentUpdate is returned when the database is unable to serialize access due to a concurrent update. +func ErrConcurrentUpdate() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusBadRequest, + GRPCCodeField: codes.Aborted, + StatusField: http.StatusText(http.StatusBadRequest), + ErrorField: "Unable to serialize access due to a concurrent update in another session", + } +} + +func ErrNoSuchTable() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusInternalServerError, + GRPCCodeField: codes.Internal, + StatusField: http.StatusText(http.StatusInternalServerError), + ErrorField: "Unable to locate the table", + } +} + +func ErrNoSuchColumn() *herodot.DefaultError { + return &herodot.DefaultError{ + CodeField: http.StatusInternalServerError, + GRPCCodeField: codes.Internal, + StatusField: http.StatusText(http.StatusInternalServerError), + ErrorField: "Unable to locate the column", + } +} + +func handlePostgres(err error, sqlState string) error { + switch sqlState { + case "23505": // "unique_violation" + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case "40001", // "serialization_failure" in CRDB + "CR000": // "serialization_failure" + return errors.WithStack(ErrConcurrentUpdate().WithWrap(err)) + case "42P01": // "no such table" + return errors.WithStack(ErrNoSuchTable().WithWrap(err)) + case "42703": // "no such column" + return errors.WithStack(ErrNoSuchColumn().WithWrap(err)) + } + return errors.WithStack(err) +} + +// HandleError returns the right sqlcon.Err* depending on the input error. +func HandleError(err error) error { + if err == nil { + return nil + } + + type stater = interface { + error + SQLState() string + } + + if stderrs.Is(err, sql.ErrNoRows) { + return errors.WithStack(ErrNoRows()) + } + if e, ok := stderrs.AsType[stater](err); ok { + return errors.WithStack(handlePostgres(err, e.SQLState())) + } + if e, ok := stderrs.AsType[*pgconn.PgError](err); ok { + return errors.WithStack(handlePostgres(err, e.Code)) + } + if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok { + switch e.Number { + case 1062: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_dup_entry + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case 1146: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_no_such_table + return errors.WithStack(ErrNoSuchTable().WithWrap(e)) + case 1054: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_bad_field_error + return errors.WithStack(ErrNoSuchColumn().WithWrap(e)) + case 1213: // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_lock_deadlock + return errors.WithStack(ErrConcurrentUpdate().WithWrap(e)) + } + } + if e, ok := stderrs.AsType[*sqlite.Error](err); ok { + return handleSqlite(e, err) + } + + return errors.WithStack(err) +} + +// IsForeignKeyViolation reports whether err is a foreign-key constraint +// violation, e.g. deleting a parent row that a child row still references +// under ON DELETE RESTRICT or NO ACTION. +func IsForeignKeyViolation(err error) bool { + if err == nil { + return false + } + + type stater = interface { + error + SQLState() string + } + + if e, ok := stderrs.AsType[stater](err); ok { + return e.SQLState() == "23503" // "foreign_key_violation" in Postgres / CockroachDB + } + if e, ok := stderrs.AsType[*pgconn.PgError](err); ok { + return e.Code == "23503" + } + if e, ok := stderrs.AsType[*mysql.MySQLError](err); ok { + // 1451: cannot delete or update a parent row (a foreign key constraint fails). + // https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_row_is_referenced_2 + return e.Number == 1451 + } + if e, ok := stderrs.AsType[*sqlite.Error](err); ok { + return isSqliteForeignKeyViolation(e) + } + return false +} diff --git a/oryx/sqlcon/error_sqlite.go b/oryx/sqlcon/error_sqlite.go new file mode 100644 index 0000000..cdd075f --- /dev/null +++ b/oryx/sqlcon/error_sqlite.go @@ -0,0 +1,47 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "strings" + + "github.com/pkg/errors" + "modernc.org/sqlite" + sqlite3lib "modernc.org/sqlite/lib" +) + +// isSqliteForeignKeyViolation reports whether e is an SQLite foreign-key +// constraint failure. SQLite's extended result code for this case is not +// reported consistently by the driver, so we match on the stable error +// message instead. +func isSqliteForeignKeyViolation(e *sqlite.Error) bool { + return strings.Contains(e.Error(), "FOREIGN KEY constraint failed") +} + +// handleSqlite handles the error iff (if and only if) it is an sqlite error +func handleSqlite(e *sqlite.Error, err error) error { + // Code() returns the full extended error code. + switch e.Code() { + case sqlite3lib.SQLITE_CONSTRAINT_UNIQUE, + sqlite3lib.SQLITE_CONSTRAINT_PRIMARYKEY: + return errors.WithStack(ErrUniqueViolation().WithWrap(err)) + case sqlite3lib.SQLITE_ERROR: + if strings.Contains(e.Error(), "no such table") { + return errors.WithStack(ErrNoSuchTable().WithWrap(err)) + } + case sqlite3lib.SQLITE_LOCKED, + sqlite3lib.SQLITE_BUSY, + sqlite3lib.SQLITE_BUSY_RECOVERY, + sqlite3lib.SQLITE_BUSY_SNAPSHOT, + sqlite3lib.SQLITE_BUSY_TIMEOUT: + return errors.WithStack(ErrConcurrentUpdate().WithWrap(err)) + } + + if strings.HasPrefix(e.Error(), "no such column:") || + strings.Contains(e.Error(), "has no column named") { + return errors.WithStack(ErrNoSuchColumn().WithWrap(err)) + } + + return errors.WithStack(err) +} diff --git a/oryx/sqlcon/message.go b/oryx/sqlcon/message.go new file mode 100644 index 0000000..d6b44e8 --- /dev/null +++ b/oryx/sqlcon/message.go @@ -0,0 +1,87 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +// HelpMessage returns a string explaining how to set up SQL using environment variables. +func HelpMessage() string { + return `- DATABASE_URL: A DSN to a persistent backend. Various backends are supported: + + - Changes are lost on process death (ephemeral storage): + + - Memory: If DATABASE_URL is "memory", data will be written to memory and is lost when you restart this instance. + Example: DATABASE_URL=memory + + - Changes are kept after process death (persistent storage): + + - SQL Databases: Officially, PostgreSQL, MySQL and CockroachDB are supported. This project works best with PostgreSQL. + + - PostgreSQL: If DATABASE_URL is a DSN starting with postgres://, PostgreSQL will be used as storage backend. + Example: DATABASE_URL=postgres://user:password@host:123/database + + Additionally, the following query/DSN parameters are supported: + + * max_conns (number): Sets the maximum number of open connections to the database. Defaults to the number of CPU cores times 2. + * max_idle_conns (number): Sets the maximum number of connections in the idle. Defaults to the number of CPU cores. + * max_conn_lifetime (duratino): Sets the maximum amount of time ("ms", "s", "m", "h") a connection may be reused. + Defaults to 0s (disabled). + * sslmode (string): Whether or not to use SSL (default is require) + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + * fallback_application_name (string): An application_name to fall back to if one isn't provided. + * connect_timeout (number): Maximum wait for connection, in seconds. Zero or + not specified means wait indefinitely. + * sslcert (string): Cert file location. The file must contain PEM encoded data. + * sslkey (string): Key file location. The file must contain PEM encoded data. + * sslrootcert (string): The location of the root certificate file. The file + must contain PEM encoded data. + Example: DATABASE_URL=postgres://user:password@host:123/database?sslmode=verify-full + + - MySQL: If DATABASE_URL is a DSN starting with mysql:// MySQL will be used as storage backend. + Be aware that the ?parseTime=true parameter is mandatory, or timestamps will not work. + Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true + + Additionally, the following query/DSN parameters are supported: + * collation (string): Sets the collation used for client-server interaction on connection. In contrast to charset, + collation does not issue additional queries. If the specified collation is unavailable on the target server, + the connection will fail. + * loc (string): Sets the location for time.Time values. Note that this sets the location for time.Time values + but does not change MySQL's time_zone setting. For that set the time_zone DSN parameter. Please keep in mind, + that param values must be url.QueryEscape'ed. Alternatively you can manually replace the / with %2F. + For example US/Pacific would be loc=US%2FPacific. + * maxAllowedPacket (number): Max packet size allowed in bytes. The default value is 4 MiB and should be + adjusted to match the server settings. maxAllowedPacket=0 can be used to automatically fetch the max_allowed_packet variable from server on every connection. + * readTimeout (duration): I/O read timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + * timeout (duration): Timeout for establishing connections, aka dial timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + * tls (bool / string): tls=true enables TLS / SSL encrypted connection to the server. Use skip-verify if + you want to use a self-signed or invalid certificate (server side). + * writeTimeout (duration): I/O write timeout. The value must be a decimal number with a unit suffix + ("ms", "s", "m", "h"), such as "30s", "0.5m" or "1m30s". + Example: DATABASE_URL=mysql://user:password@tcp(host:123)/database?parseTime=true&writeTimeout=123s + + - CockroachDB: If DATABASE_URL is a DSN starting with cockroach://, CockroachDB will be used as storage backend. + Example: DATABASE_URL=cockroach://user:password@host:123/database + + Additionally, the following query/DSN parameters are supported: + * sslmode (string): Whether or not to use SSL (default is require) + * disable - No SSL + * require - Always SSL (skip verification) + * verify-ca - Always SSL (verify that the certificate presented by the + server was signed by a trusted CA) + * verify-full - Always SSL (verify that the certification presented by + the server was signed by a trusted CA and the server host name + matches the one in the certificate) + * application_name (string): An initial value for the application_name session variable. + * sslcert (string): Cert file location. The file must contain PEM encoded data. + * sslkey (string): Key file location. The file must contain PEM encoded data. + * sslrootcert (string): The location of the root certificate file. The file + must contain PEM encoded data. + Example: DATABASE_URL=cockroach://user:password@host:123/database?sslmode=verify-full` +} diff --git a/oryx/sqlcon/parse_opts.go b/oryx/sqlcon/parse_opts.go new file mode 100644 index 0000000..b8adccd --- /dev/null +++ b/oryx/sqlcon/parse_opts.go @@ -0,0 +1,118 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlcon + +import ( + "fmt" + "net/url" + "strconv" + "strings" + "time" + + "github.com/ory/x/logrusx" +) + +// ParseConnectionOptions parses values for max_conns, max_idle_conns, max_conn_lifetime from DSNs. +// It also returns the URI without those query parameters. +func ParseConnectionOptions(l *logrusx.Logger, dsn string) (maxConns int, maxIdleConns int, maxConnLifetime, maxIdleConnTime time.Duration, cleanedDSN string) { + maxConns = maxParallelism() * 2 + maxIdleConns = maxParallelism() + cleanedDSN = dsn + + hostPath, rawQuery, ok := strings.Cut(dsn, "?") + if !ok { + l. + WithField("sql_max_connections", maxConns). + WithField("sql_max_idle_connections", maxIdleConns). + WithField("sql_max_connection_lifetime", maxConnLifetime). + WithField("sql_max_idle_connection_time", maxIdleConnTime). + Debugf("No SQL connection options have been defined, falling back to default connection options.") + return + } + + query, err := url.ParseQuery(rawQuery) + if err != nil { + l. + WithField("sql_max_connections", maxConns). + WithField("sql_max_idle_connections", maxIdleConns). + WithField("sql_max_connection_lifetime", maxConnLifetime). + WithField("sql_max_idle_connection_time", maxIdleConnTime). + WithError(err). + Warnf("Unable to parse SQL DSN query, falling back to default connection options.") + return + } + + if v := query.Get("max_conns"); v != "" { + s, err := strconv.ParseInt(v, 10, 64) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conns" value %v could not be parsed to int, falling back to default value %d`, v, maxConns) + } else { + maxConns = int(s) + } + query.Del("max_conns") + } + + if v := query.Get("max_idle_conns"); v != "" { + s, err := strconv.ParseInt(v, 10, 64) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_idle_conns" value %v could not be parsed to int, falling back to default value %d`, v, maxIdleConns) + } else { + maxIdleConns = int(s) + } + query.Del("max_idle_conns") + } + + if v := query.Get("max_conn_lifetime"); v != "" { + s, err := time.ParseDuration(v) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conn_lifetime" value %v could not be parsed to duration, falling back to default value %d`, v, maxConnLifetime) + } else { + maxConnLifetime = s + } + query.Del("max_conn_lifetime") + } + + if v := query.Get("max_conn_idle_time"); v != "" { + s, err := time.ParseDuration(v) + if err != nil { + l.WithError(err).Warnf(`SQL DSN query parameter "max_conn_idle_time" value %v could not be parsed to duration, falling back to default value %d`, v, maxIdleConnTime) + } else { + maxIdleConnTime = s + } + query.Del("max_conn_idle_time") + } + cleanedDSN = fmt.Sprintf("%s?%s", hostPath, query.Encode()) + + return +} + +// FinalizeDSN will return a finalized DSN URI. +func FinalizeDSN(l *logrusx.Logger, dsn string) string { + if !strings.HasPrefix(dsn, "mysql://") { + return dsn + } + + var q url.Values + hostPath, query, ok := strings.Cut(dsn, "?") + + if !ok { + q = make(url.Values) + } else { + var err error + q, err = url.ParseQuery(query) + if err != nil { + l.WithError(err).Warnf("Unable to parse SQL DSN query, could not finalize the DSN URI.") + return dsn + } + } + + q.Set("multiStatements", "true") + q.Set("parseTime", "true") + + // This causes an UPDATE to return the number of matching rows instead of + // the number of rows changed. This ensures compatibility with PostgreSQL and SQLite behavior. + q.Set("clientFoundRows", "true") + + return fmt.Sprintf("%s?%s", hostPath, q.Encode()) +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json new file mode 100644 index 0000000..51b3ae7 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=cockroach.json @@ -0,0 +1,14 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, gen_random_uuid(), ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)" +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json new file mode 100644 index 0000000..db458b9 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryArgs-case=testModel.json @@ -0,0 +1,14 @@ +{ + "TableName": "\"test_models\"", + "ColumnsDecl": "\"created_at\", \"id\", \"int\", \"nid\", \"null_time_ptr\", \"string\", \"updated_at\"", + "Columns": [ + "created_at", + "id", + "int", + "nid", + "null_time_ptr", + "string", + "updated_at" + ], + "Placeholders": "(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?),\n(?, ?, ?, ?, ?, ?, ?)" +} diff --git a/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json new file mode 100644 index 0000000..c5bdc38 --- /dev/null +++ b/oryx/sqlxx/batch/.snapshots/Test_buildInsertQueryValues-case=testModel-case=cockroach.json @@ -0,0 +1,16 @@ +[ + "0001-01-01T00:00:00Z", + "0001-01-01T00:00:00Z", + "string", + 42, + null, + { + "ID": "00000000-0000-0000-0000-000000000000", + "NID": "00000000-0000-0000-0000-000000000000", + "String": "string", + "Int": 42, + "NullTimePtr": null, + "created_at": "0001-01-01T00:00:00Z", + "updated_at": "0001-01-01T00:00:00Z" + } +] diff --git a/oryx/sqlxx/batch/create.go b/oryx/sqlxx/batch/create.go new file mode 100644 index 0000000..ae01482 --- /dev/null +++ b/oryx/sqlxx/batch/create.go @@ -0,0 +1,298 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package batch + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "sort" + "strings" + "time" + + "github.com/jmoiron/sqlx/reflectx" + + "github.com/ory/x/dbal" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/pop/v6" + + "github.com/ory/x/otelx" + "github.com/ory/x/sqlcon" + + "github.com/ory/x/sqlxx" +) + +type ( + insertQueryArgs struct { + TableName string + ColumnsDecl string + Columns []string + Placeholders string + } + quoter interface { + Quote(key string) string + } + TracerConnection struct { + Tracer *otelx.Tracer + Connection *pop.Connection + } +) + +func buildInsertQueryArgs[T any](ctx context.Context, dialect string, mapper *reflectx.Mapper, quoter quoter, models []*T) insertQueryArgs { + var ( + v T + model = pop.NewModel(v, ctx) + + columns []string + quotedColumns []string + placeholders []string + placeholderRow []string + ) + + for _, col := range model.Columns().Cols { + columns = append(columns, col.Name) + placeholderRow = append(placeholderRow, "?") + } + + // We sort for the sole reason that the test snapshots are deterministic. + sort.Strings(columns) + + for _, col := range columns { + quotedColumns = append(quotedColumns, quoter.Quote(col)) + } + + // We generate a list (for every row one) of VALUE statements here that + // will be substituted by their column values later: + // + // (?, ?, ?, ?), + // (?, ?, ?, ?), + // (?, ?, ?, ?) + for _, m := range models { + m := reflect.ValueOf(m) + + pl := make([]string, len(placeholderRow)) + copy(pl, placeholderRow) + + // PostgreSQL-compatible databases can generate UUIDs using + // "gen_random_uuid()", which ends up in a VALUE statement of: + // + // (gen_random_uuid(), ?, ?, ?), + for k := range placeholderRow { + if columns[k] != "id" { + continue + } + + field := mapper.FieldByName(m, columns[k]) + val, ok := field.Interface().(uuid.UUID) + if !ok { + continue + } + + if val == uuid.Nil && dbal.IsPostgresCompatible(dialect) { + pl[k] = "gen_random_uuid()" + break + } + } + + placeholders = append(placeholders, fmt.Sprintf("(%s)", strings.Join(pl, ", "))) + } + + return insertQueryArgs{ + TableName: quoter.Quote(model.TableName()), + ColumnsDecl: strings.Join(quotedColumns, ", "), + Columns: columns, + Placeholders: strings.Join(placeholders, ",\n"), + } +} + +func buildInsertQueryValues[T any](dialect string, mapper *reflectx.Mapper, columns []string, models []*T, nowFunc func() time.Time) (values []any, err error) { + for _, m := range models { + m := reflect.ValueOf(m) + + now := nowFunc() + // Append model fields to args + for _, c := range columns { + field := mapper.FieldByName(m, c) + + switch c { + case "created_at": + if pop.IsZeroOfUnderlyingType(field.Interface()) { + field.Set(reflect.ValueOf(now)) + } + case "updated_at": + field.Set(reflect.ValueOf(now)) + case "id": + if value, ok := field.Interface().(uuid.UUID); ok && value != uuid.Nil { + break // breaks switch, not for + } else if value, ok := field.Interface().(string); ok && len(value) > 0 { + break // breaks switch, not for + } else if dbal.IsPostgresCompatible(dialect) { + // This is a special case: + // 1. We're using a PostgreSQL-compatible database. + // 2. It's the primary key field ("ID") + // 3. A UUID was not yet set. + // + // If all these conditions meet, the VALUE statement will look as such: + // + // (gen_random_uuid(), ?, ?, ?, ...) + // + // For that reason, we do not add the ID value to the list of arguments, + // because one of the arguments is using a built-in and thus doesn't need a value. + continue // break switch, not for + } + + id, err := uuid.NewV4() + if err != nil { + return nil, err + } + field.Set(reflect.ValueOf(id)) + } + + values = append(values, field.Interface()) + + // Special-handling for *sqlxx.NullTime: mapper.FieldByName sets this to a zero time.Time, + // but we want a nil pointer instead. + if i, ok := field.Interface().(*sqlxx.NullTime); ok { + if time.Time(*i).IsZero() { + field.Set(reflect.Zero(field.Type())) + } + } + } + } + + return values, nil +} + +type createOptions struct { + onConflict string +} + +type option func(*createOptions) + +func OnConflictDoNothing() func(*createOptions) { + return func(o *createOptions) { + o.onConflict = "ON CONFLICT DO NOTHING" + } +} + +// CreateFromSlice is a helper around Create that accepts a slice of models +// instead of a slice of model pointers. +func CreateFromSlice[T any](ctx context.Context, p *TracerConnection, models []T, opts ...option) (err error) { + var ptrs []*T + for k := range models { + ptrs = append(ptrs, &models[k]) + } + return Create(ctx, p, ptrs, opts...) +} + +// Create batch-inserts the given models into the database using a single INSERT statement. +// The models are either all created or none. +func Create[T any](ctx context.Context, p *TracerConnection, models []*T, opts ...option) (err error) { + ctx, span := p.Tracer.Tracer().Start(ctx, "persistence.sql.batch.Create") + defer otelx.End(span, &err) + + if len(models) == 0 { + return nil + } + + options := &createOptions{} + for _, opt := range opts { + opt(options) + } + + var v T + model := pop.NewModel(v, ctx) + + conn := p.Connection + quoter, ok := conn.Dialect.(quoter) + if !ok { + return errors.Errorf("store is not a quoter: %T", conn.Store) + } + + queryArgs := buildInsertQueryArgs(ctx, conn.Dialect.Name(), conn.TX.Mapper, quoter, models) + values, err := buildInsertQueryValues(conn.Dialect.Name(), conn.TX.Mapper, queryArgs.Columns, models, func() time.Time { return time.Now().UTC().Truncate(time.Microsecond) }) + if err != nil { + return err + } + + var returningClause string + if conn.Dialect.Name() != dbal.DriverMySQL { + // PostgreSQL, CockroachDB, YugabyteDB, and SQLite support RETURNING. + returningClause = fmt.Sprintf("RETURNING %s", model.IDField()) + } + + query := conn.Dialect.TranslateSQL(fmt.Sprintf( + "INSERT INTO %s (%s) VALUES\n%s\n%s\n%s", + queryArgs.TableName, + queryArgs.ColumnsDecl, + queryArgs.Placeholders, + options.onConflict, + returningClause, + )) + + rows, err := conn.TX.QueryContext(ctx, query, values...) + if err != nil { + return sqlcon.HandleError(err) + } + defer rows.Close() + + // Hydrate the models from the RETURNING clause. + // + // Databases not supporting RETURNING will just return 0 rows. + count := 0 + for rows.Next() { + if err := setModelID(rows, pop.NewModel(models[count], ctx)); err != nil { + return err + } + count++ + } + + if err := rows.Err(); err != nil { + return sqlcon.HandleError(err) + } + + return sqlcon.HandleError(err) +} + +// setModelID was copy & pasted from pop. It basically sets +// the primary key to the given value read from the SQL row. +func setModelID(row *sql.Rows, model *pop.Model) error { + el := reflect.ValueOf(model.Value).Elem() + fbn := el.FieldByName("ID") + if !fbn.IsValid() { + return errors.New("model does not have a field named id") + } + + pkt, err := model.PrimaryKeyType() + if err != nil { + return errors.WithStack(err) + } + + switch pkt { + case "UUID": + var id uuid.UUID + if err := row.Scan(&id); err != nil { + return errors.WithStack(err) + } + fbn.Set(reflect.ValueOf(id)) + default: + var id interface{} + if err := row.Scan(&id); err != nil { + return errors.WithStack(err) + } + v := reflect.ValueOf(id) + switch fbn.Kind() { + case reflect.Int, reflect.Int64: + fbn.SetInt(v.Int()) + default: + fbn.Set(reflect.ValueOf(id)) + } + } + + return nil +} diff --git a/oryx/sqlxx/expand.go b/oryx/sqlxx/expand.go new file mode 100644 index 0000000..8f9020d --- /dev/null +++ b/oryx/sqlxx/expand.go @@ -0,0 +1,34 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +// Expandable controls what fields to expand for projects. +type Expandable string + +// Expandables is a list of Expandable values. +type Expandables []Expandable + +// String returns a string representation of the Expandable. +func (e Expandable) String() string { + return string(e) +} + +// ToEager returns the fields used by pop's Eager command. +func (e Expandables) ToEager() []string { + var s []string + for _, e := range e { + s = append(s, e.String()) + } + return s +} + +// Has returns true if the Expandable is in the list. +func (e Expandables) Has(search Expandable) bool { + for _, e := range e { + if e == search { + return true + } + } + return false +} diff --git a/oryx/sqlxx/sqlxx.go b/oryx/sqlxx/sqlxx.go new file mode 100644 index 0000000..e472095 --- /dev/null +++ b/oryx/sqlxx/sqlxx.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +import ( + "fmt" + "net/url" + "reflect" + "slices" + "strings" + + "github.com/pkg/errors" +) + +func keys(t any, exclude []string) []string { + tt := reflect.TypeOf(t) + if tt.Kind() == reflect.Pointer { + tt = tt.Elem() + } + ks := make([]string, 0, tt.NumField()) + for i := range tt.NumField() { + f := tt.Field(i) + key, _, _ := strings.Cut(f.Tag.Get("db"), ",") + if key != "" && key != "-" && !slices.Contains(exclude, key) { + ks = append(ks, key) + } + } + return ks +} + +// NamedInsertArguments returns columns and arguments for SQL INSERT statements based on a struct's tags. Does +// not work with nested structs or maps! +// +// type st struct { +// Foo string `db:"foo"` +// Bar string `db:"bar,omitempty"` +// Baz string `db:"-"` +// Zab string +// } +// columns, arguments := NamedInsertArguments(new(st)) +// query := fmt.Sprintf("INSERT INTO foo (%s) VALUES (%s)", columns, arguments) +// // INSERT INTO foo (foo, bar) VALUES (:foo, :bar) +func NamedInsertArguments(t any, exclude ...string) (columns string, arguments string) { + keys := keys(t, exclude) + return strings.Join(keys, ", "), + ":" + strings.Join(keys, ", :") +} + +// NamedUpdateArguments returns columns and arguments for SQL UPDATE statements based on a struct's tags. Does +// not work with nested structs or maps! +// +// type st struct { +// Foo string `db:"foo"` +// Bar string `db:"bar,omitempty"` +// Baz string `db:"-"` +// Zab string +// } +// query := fmt.Sprintf("UPDATE foo SET %s", NamedUpdateArguments(new(st))) +// // UPDATE foo SET foo=:foo, bar=:bar +func NamedUpdateArguments(t any, exclude ...string) string { + keys := keys(t, exclude) + statements := make([]string, len(keys)) + + for k, key := range keys { + statements[k] = fmt.Sprintf("%s=:%s", key, key) + } + + return strings.Join(statements, ", ") +} + +func OnConflictDoNothing(dialect string, columnNoop string) string { + if dialect == "mysql" { + return fmt.Sprintf(" ON DUPLICATE KEY UPDATE `%s` = `%s` ", columnNoop, columnNoop) + } else { + return ` ON CONFLICT DO NOTHING ` + } +} + +// ExtractSchemeFromDSN returns the scheme (e.g. `mysql`, `postgres`, etc) component in a DSN string, +// as well as the remaining part of the DSN after the scheme separator. +// It is an error to not have a scheme present. +// This makes sense in the context of a DSN to be able to identify which database is in use. +func ExtractSchemeFromDSN(dsn string) (string, string, error) { + scheme, afterSchemeSeparator, schemeSeparatorFound := strings.Cut(dsn, "://") + if !schemeSeparatorFound { + return "", "", errors.New("invalid DSN: missing scheme separator") + } + if scheme == "" { + return "", "", errors.New("invalid DSN: empty scheme") + } + + return scheme, afterSchemeSeparator, nil +} + +// ExtractDbNameFromDSN returns the database name component in a DSN string. +func ExtractDbNameFromDSN(dsn string) (string, error) { + _, afterScheme, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", err + } + + _, afterSlash, slashFound := strings.Cut(afterScheme, "/") + if !slashFound { + return "", nil + } + + dbName, _, _ := strings.Cut(afterSlash, "?") + return dbName, nil +} + +// ReplaceSchemeInDSN replaces the scheme (e.g. `mysql`, `postgres`, etc) in a DSN string with another one. +// This is necessary for example when using `cockroach` as a scheme, but using the postgres driver to connect to the database, +// and this driver only accepts `postgres` as a scheme. +func ReplaceSchemeInDSN(dsn string, newScheme string) (string, error) { + _, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", errors.WithStack(err) + } + + return newScheme + "://" + afterSchemeSeparator, nil +} + +// DSNRedacted parses a database DSN and returns a redacted form as a string. +// It replaces any password with "xxxxx" just like `url.Redacted()`. +// Only the password is redacted, not the username. +// This function is necessary because MySQL uses a DSN format not compatible with `url.Parse`. +// Additionally and as a consequence of the point above, the scheme is expected to be present and non-empty. +// This function is less strict that `url.Parse` in the case of MySQL. +// It also does not escape any characters in the username, whereas `url.String()`/`url.Redacted` does. +func DSNRedacted(dsn string) (string, error) { + scheme, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn) + if err != nil { + return "", errors.WithStack(err) + } + + // If this is not MySQL, we simply delegate the work to `url.Parse`. + if scheme != "mysql" { + u, err := url.Parse(dsn) + if err != nil { + return "", errors.WithStack(err) + } + return u.Redacted(), nil + } + + // MySQL has a weird DSN syntax not conforming to a standard URL, of the form: + // `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]` + // We only need to parse up to `@` in order to redact the password. The rest is left as-is. + + usernamePassword, afterUsernamePassword, usernamePasswordSeparatorFound := strings.Cut(afterSchemeSeparator, "@") + if !usernamePasswordSeparatorFound { + afterUsernamePassword = afterSchemeSeparator + } + + username, password, hasPassword := strings.Cut(usernamePassword, ":") + // We only insert a redacted password in the final result if a password was provided in the input. + // This behavior matches the one of `url.Redacted()`. + if hasPassword { + password = ":xxxxx" + } + + res := scheme + "://" + if usernamePasswordSeparatorFound { + res += username + password + "@" + } + res += afterUsernamePassword + return res, nil +} + +func StripQueryParamsFromDSN(dsn string) string { + before, _, _ := strings.Cut(dsn, "?") + return before +} diff --git a/oryx/sqlxx/types.go b/oryx/sqlxx/types.go new file mode 100644 index 0000000..5f6f4eb --- /dev/null +++ b/oryx/sqlxx/types.go @@ -0,0 +1,632 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package sqlxx + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "fmt" + "slices" + "strings" + "time" + + "github.com/tidwall/gjson" + + "github.com/pkg/errors" +) + +// Duration represents a JSON and SQL compatible time.Duration. +// swagger:type string +type Duration time.Duration + +// MarshalJSON returns m as the JSON encoding of m. +func (ns Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(ns).String()) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *Duration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := time.ParseDuration(s) + if err != nil { + return err + } + + *ns = Duration(p) + return nil +} + +// StringSliceJSONFormat represents []string{} which is encoded to/from JSON for SQL storage. +// swagger:type array +type StringSliceJSONFormat []string + +// Scan implements the Scanner interface. +func (m *StringSliceJSONFormat) Scan(value interface{}) error { + var val string + switch v := value.(type) { + case nil: + *m = StringSliceJSONFormat{} + return nil + case string: + val = v + case []byte: + val = string(v) + default: + return errors.Errorf("cannot scan %#v into StringSliceJSONFormat", value) + } + if len(val) == 0 { + val = "[]" + } + + if parsed := gjson.Parse(val); parsed.Type == gjson.Null { + val = "[]" + } else if !parsed.IsArray() { + return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + return errors.WithStack(json.Unmarshal([]byte(val), &m)) +} + +// Value implements the driver Valuer interface. +func (m StringSliceJSONFormat) Value() (driver.Value, error) { + if len(m) == 0 { + return "[]", nil + } + + encoded, err := json.Marshal(&m) + return string(encoded), errors.WithStack(err) +} + +// StringSlicePipeDelimiter de/encodes the string slice to/from a SQL string. +type StringSlicePipeDelimiter []string + +// Scan implements the Scanner interface. +func (n *StringSlicePipeDelimiter) Scan(value interface{}) error { + var s sql.NullString + if err := s.Scan(value); err != nil { + return err + } + *n = scanStringSlice('|', s.String) + return nil +} + +// Value implements the driver Valuer interface. +func (n StringSlicePipeDelimiter) Value() (driver.Value, error) { + return valueStringSlice('|', n), nil +} + +func scanStringSlice(delimiter rune, value interface{}) []string { + escaped := false + s := fmt.Sprintf("%s", value) + splitted := strings.FieldsFunc(s, func(r rune) bool { + if r == '\\' { + escaped = !escaped + } else if escaped && r != delimiter { + escaped = false + } + return !escaped && r == delimiter + }) + for k, v := range splitted { + splitted[k] = strings.ReplaceAll(v, "\\"+string(delimiter), string(delimiter)) + } + return splitted +} + +func valueStringSlice(delimiter rune, value []string) string { + replace := make([]string, len(value)) + for k, v := range value { + replace[k] = strings.ReplaceAll(v, string(delimiter), "\\"+string(delimiter)) + } + return strings.Join(replace, string(delimiter)) +} + +// NullBool represents a bool that may be null. +// NullBool implements the Scanner interface so +// swagger:type bool +// swagger:model nullBool +type NullBool struct { + Bool bool + Valid bool // Valid is true if Bool is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullBool) Scan(value interface{}) error { + d := sql.NullBool{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Bool = d.Bool + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullBool) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Bool, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullBool) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + return json.Marshal(ns.Bool) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullBool) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Bool)) +} + +// FalsyNullBool represents a bool that may be null. +// It JSON decodes to false if null. +// +// swagger:type bool +// swagger:model falsyNullBool +type FalsyNullBool struct { + Bool bool + Valid bool // Valid is true if Bool is not NULL +} + +// Scan implements the Scanner interface. +func (ns *FalsyNullBool) Scan(value interface{}) error { + d := sql.NullBool{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Bool = d.Bool + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns FalsyNullBool) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Bool, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns FalsyNullBool) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("false"), nil + } + return json.Marshal(ns.Bool) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *FalsyNullBool) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Bool)) +} + +// swagger:type string +// swagger:model nullString +type NullString string + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullString) MarshalJSON() ([]byte, error) { + return json.Marshal(string(ns)) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullString) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal(data, (*string)(ns))) +} + +// Scan implements the Scanner interface. +func (ns *NullString) Scan(value interface{}) error { + var v sql.NullString + if err := (&v).Scan(value); err != nil { + return err + } + *ns = NullString(v.String) + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullString) Value() (driver.Value, error) { + if len(ns) == 0 { + return sql.NullString{}.Value() + } + return sql.NullString{Valid: true, String: string(ns)}.Value() +} + +// String implements the Stringer interface. +func (ns NullString) String() string { + return string(ns) +} + +// NullTime implements sql.NullTime functionality. +// +// swagger:model nullTime +// required: false +type NullTime time.Time + +// Scan implements the Scanner interface. +func (ns *NullTime) Scan(value interface{}) error { + var v sql.NullTime + if err := (&v).Scan(value); err != nil { + return err + } + *ns = NullTime(v.Time.UTC()) + return nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullTime) MarshalJSON() ([]byte, error) { + var t *time.Time + if !time.Time(ns).IsZero() { + tt := time.Time(ns) + t = &tt + } + return json.Marshal(t) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullTime) UnmarshalJSON(data []byte) error { + var t time.Time + if err := json.Unmarshal(data, &t); err != nil { + return err + } + *ns = NullTime(t) + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullTime) Value() (driver.Value, error) { + return sql.NullTime{Valid: !time.Time(ns).IsZero(), Time: time.Time(ns).UTC()}.Value() +} + +// DeepCopy implements github.com/mohae/deepcopy.Interface. +// +// NullTime wraps time.Time, whose fields are unexported. The default +// reflection-based deep copy in mohae/deepcopy skips unexported fields, which +// would zero out the timestamp. Returning a pointer to a copy preserves the +// value. The method uses a pointer receiver so that mohae/deepcopy returns +// the matching *NullTime type when it finds one as a struct field. +func (ns *NullTime) DeepCopy() any { + if ns == nil { + return (*NullTime)(nil) + } + cpy := *ns + return &cpy +} + +// MapStringInterface represents a map[string]interface that works well with JSON, SQL, and Swagger. +type MapStringInterface map[string]interface{} + +// Scan implements the Scanner interface. +func (n *MapStringInterface) Scan(value interface{}) error { + v := fmt.Sprintf("%s", value) + if len(v) == 0 { + return nil + } + return errors.WithStack(json.Unmarshal([]byte(v), n)) +} + +// Value implements the driver Valuer interface. +func (n MapStringInterface) Value() (driver.Value, error) { + value, err := json.Marshal(n) + if err != nil { + return nil, errors.WithStack(err) + } + return string(value), nil +} + +// JSONArrayRawMessage represents a json.RawMessage which only accepts arrays that works well with JSON, SQL, and Swagger. +type JSONArrayRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *JSONArrayRawMessage) Scan(value interface{}) error { + val := fmt.Sprintf("%s", value) + if len(val) == 0 { + val = "[]" + } + + if parsed := gjson.Parse(val); parsed.Type == gjson.Null { + val = "[]" + } else if !parsed.IsArray() { + return errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + *m = []byte(val) + return nil +} + +// Value implements the driver Valuer interface. +func (m JSONArrayRawMessage) Value() (driver.Value, error) { + if len(m) == 0 { + return "[]", nil + } + + if parsed := gjson.ParseBytes(m); parsed.Type == gjson.Null { + return "[]", nil + } else if !parsed.IsArray() { + return nil, errors.Errorf("expected JSON value to be an array but got type: %s", parsed.Type.String()) + } + + return string(m), nil +} + +// JSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger. +type JSONRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *JSONRawMessage) Scan(value interface{}) error { + switch v := value.(type) { + case []byte: + *m = slices.Clone(v) + case string: + *m = JSONRawMessage(v) + case nil: + *m = JSONRawMessage("null") + default: + return errors.Errorf("cannot scan %T into JSONRawMessage", value) + } + return nil +} + +// Value implements the driver Valuer interface. +func (m JSONRawMessage) Value() (driver.Value, error) { + if len(m) == 0 { + return "null", nil + } + return string(m), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m JSONRawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *JSONRawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} + +// NullJSONRawMessage represents a json.RawMessage that works well with JSON, SQL, and Swagger and is NULLable- +// +// swagger:model nullJsonRawMessage +type NullJSONRawMessage json.RawMessage + +// Scan implements the Scanner interface. +func (m *NullJSONRawMessage) Scan(value any) error { + return (*JSONRawMessage)(m).Scan(value) +} + +// Value implements the driver Valuer interface. +func (m NullJSONRawMessage) Value() (driver.Value, error) { + if len(m) == 0 || string(m) == "null" { + return nil, nil + } + return string(m), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m NullJSONRawMessage) MarshalJSON() ([]byte, error) { + if len(m) == 0 { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *NullJSONRawMessage) UnmarshalJSON(data []byte) error { + if m == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *m = append((*m)[0:0], data...) + return nil +} + +// NullJSONObject represents a nullable JSON object (always key-value pairs). +// Use this for fields that are always objects (e.g., HTTP headers). +// For arbitrary JSON that can be strings, arrays, or objects, use NullJSONRawMessage. +// +// swagger:type object +type NullJSONObject json.RawMessage + +// Scan implements the Scanner interface. +func (m *NullJSONObject) Scan(value any) error { + return (*NullJSONRawMessage)(m).Scan(value) +} + +// Value implements the driver Valuer interface. +func (m NullJSONObject) Value() (driver.Value, error) { + return NullJSONRawMessage(m).Value() +} + +// MarshalJSON returns m as the JSON encoding of m. +func (m NullJSONObject) MarshalJSON() ([]byte, error) { + return NullJSONRawMessage(m).MarshalJSON() +} + +// UnmarshalJSON sets *m to a copy of data. +func (m *NullJSONObject) UnmarshalJSON(data []byte) error { + return (*NullJSONRawMessage)(m).UnmarshalJSON(data) +} + +// JSONScan is a generic helper for retrieving a SQL JSON-encoded value. +func JSONScan(dst, value any) error { + // Note: raw is a string (not []byte) because the MySQL driver reuses byte slices across scans. + // Using strings avoids the need to manually copy the byte slice. + var raw string + switch v := value.(type) { + case nil: + raw = "null" + case string: + raw = v + case []byte: + raw = string(v) + default: + return fmt.Errorf("unable to scan type %T as JSON into %T", value, dst) + } + if err := json.Unmarshal([]byte(raw), dst); err != nil { + return fmt.Errorf("unable to decode JSON payload into %T: %w", dst, err) + } + return nil +} + +// NullInt64 represents an int64 that may be null. +// swagger:type int64 +// swagger:model nullInt64 +type NullInt64 struct { + Int int64 + Valid bool // Valid is true if Duration is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullInt64) Scan(value interface{}) error { + d := sql.NullInt64{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Int = d.Int64 + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullInt64) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return ns.Int, nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullInt64) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + return json.Marshal(ns.Int) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullInt64) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + if len(data) == 0 || string(data) == "null" { + return nil + } + ns.Valid = true + return errors.WithStack(json.Unmarshal(data, &ns.Int)) +} + +// NullDuration represents a nullable JSON and SQL compatible time.Duration. +// +// swagger:type string +// swagger:model nullDuration +type NullDuration struct { + Duration time.Duration + Valid bool +} + +// Scan implements the Scanner interface. +func (ns *NullDuration) Scan(value interface{}) error { + d := sql.NullInt64{} + if err := d.Scan(value); err != nil { + return err + } + + ns.Duration = time.Duration(d.Int64) + ns.Valid = d.Valid + return nil +} + +// Value implements the driver Valuer interface. +func (ns NullDuration) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return int64(ns.Duration), nil +} + +// MarshalJSON returns m as the JSON encoding of m. +func (ns NullDuration) MarshalJSON() ([]byte, error) { + if !ns.Valid { + return []byte("null"), nil + } + + return json.Marshal(ns.Duration.String()) +} + +// UnmarshalJSON sets *m to a copy of data. +func (ns *NullDuration) UnmarshalJSON(data []byte) error { + if ns == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + + if len(data) == 0 || string(data) == "null" { + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + p, err := time.ParseDuration(s) + if err != nil { + return err + } + + ns.Duration = p + ns.Valid = true + return nil +} + +func (ns Duration) IsZero() bool { return ns == 0 } +func (m StringSliceJSONFormat) IsZero() bool { return len(m) == 0 } +func (n StringSlicePipeDelimiter) IsZero() bool { return len(n) == 0 } +func (ns NullBool) IsZero() bool { return !ns.Valid || !ns.Bool } +func (ns FalsyNullBool) IsZero() bool { return !ns.Valid || !ns.Bool } +func (ns NullString) IsZero() bool { return len(ns) == 0 } +func (ns NullTime) IsZero() bool { return time.Time(ns).IsZero() } +func (n MapStringInterface) IsZero() bool { return len(n) == 0 } +func (m JSONArrayRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "[]" } +func (m JSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" } +func (m NullJSONRawMessage) IsZero() bool { return len(m) == 0 || string(m) == "null" } +func (ns NullInt64) IsZero() bool { return !ns.Valid || ns.Int == 0 } +func (ns NullDuration) IsZero() bool { return !ns.Valid || ns.Duration == 0 } diff --git a/oryx/stringslice/unique.go b/oryx/stringslice/unique.go new file mode 100644 index 0000000..7a649d4 --- /dev/null +++ b/oryx/stringslice/unique.go @@ -0,0 +1,20 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringslice + +// Unique returns the given string slice with unique values, preserving order. +// Consider using slices.Compact with slices.Sort instead when you don't care about order. +func Unique(i []string) []string { + u := make([]string, 0, len(i)) + m := make(map[string]struct{}, len(i)) + + for _, val := range i { + if _, ok := m[val]; !ok { + m[val] = struct{}{} + u = append(u, val) + } + } + + return u +} diff --git a/oryx/stringsx/case.go b/oryx/stringsx/case.go new file mode 100644 index 0000000..45048b3 --- /dev/null +++ b/oryx/stringsx/case.go @@ -0,0 +1,26 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "unicode" + +// ToLowerInitial converts a string's first character to lower case. +func ToLowerInitial(s string) string { + if s == "" { + return "" + } + a := []rune(s) + a[0] = unicode.ToLower(a[0]) + return string(a) +} + +// ToUpperInitial converts a string's first character to upper case. +func ToUpperInitial(s string) string { + if s == "" { + return "" + } + a := []rune(s) + a[0] = unicode.ToUpper(a[0]) + return string(a) +} diff --git a/oryx/stringsx/split.go b/oryx/stringsx/split.go new file mode 100644 index 0000000..132d20b --- /dev/null +++ b/oryx/stringsx/split.go @@ -0,0 +1,16 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "strings" + +// Splitx is a special case of strings.Split +// which returns an empty slice if the string is empty +func Splitx(s, sep string) []string { + if s == "" { + return []string{} + } + + return strings.Split(s, sep) +} diff --git a/oryx/stringsx/switch_case.go b/oryx/stringsx/switch_case.go new file mode 100644 index 0000000..dc5cb7f --- /dev/null +++ b/oryx/stringsx/switch_case.go @@ -0,0 +1,90 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import ( + "fmt" + "slices" + "strings" +) + +type ( + RegisteredCases struct { + cases []string + actual string + } + errUnknownCase struct { + *RegisteredCases + } + RegisteredPrefixes struct { + prefixes []string + actual string + } + errUnknownPrefix struct { + *RegisteredPrefixes + } +) + +var ( + ErrUnknownCase = errUnknownCase{} + ErrUnknownPrefix = errUnknownPrefix{} +) + +func SwitchExact(actual string) *RegisteredCases { + return &RegisteredCases{ + actual: actual, + } +} + +func SwitchPrefix(actual string) *RegisteredPrefixes { + return &RegisteredPrefixes{ + actual: actual, + } +} + +func (r *RegisteredCases) AddCase(cases ...string) bool { + r.cases = append(r.cases, cases...) + return slices.Contains(cases, r.actual) +} + +func (r *RegisteredPrefixes) HasPrefix(prefixes ...string) bool { + r.prefixes = append(r.prefixes, prefixes...) + return slices.ContainsFunc(prefixes, func(s string) bool { + return strings.HasPrefix(r.actual, s) + }) +} + +func (r *RegisteredCases) String() string { + return "[" + strings.Join(r.cases, ", ") + "]" +} + +func (r *RegisteredPrefixes) String() string { + return "[" + strings.Join(r.prefixes, ", ") + "]" +} + +func (r *RegisteredCases) ToUnknownCaseErr() error { + return errUnknownCase{r} +} + +func (r *RegisteredPrefixes) ToUnknownPrefixErr() error { + return errUnknownPrefix{r} +} + +func (e errUnknownCase) Error() string { + return fmt.Sprintf("expected one of %s but got %s", e.String(), e.actual) +} + +func (e errUnknownCase) Is(err error) bool { + _, ok := err.(errUnknownCase) + return ok +} + +func (e errUnknownPrefix) Error() string { + return fmt.Sprintf("expected %s to have one of the prefixes %s", e.actual, e.String()) +} + +func (e errUnknownPrefix) Is(err error) bool { + _, ok := err.(errUnknownPrefix) + return ok +} diff --git a/oryx/stringsx/truncate.go b/oryx/stringsx/truncate.go new file mode 100644 index 0000000..86c1021 --- /dev/null +++ b/oryx/stringsx/truncate.go @@ -0,0 +1,21 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package stringsx + +import "unicode/utf8" + +// TruncateByteLen returns string truncated at the end with the length specified +func TruncateByteLen(s string, length int) string { + if length <= 0 || len(s) <= length { + return s + } + + res := s[:length] + + // in case we cut in the middle of an utf8 rune, we have to remove the last byte as well until it fits + for !utf8.ValidString(res) { + res = res[:len(res)-1] + } + return res +} diff --git a/oryx/swaggerx/error.go b/oryx/swaggerx/error.go new file mode 100644 index 0000000..e825145 --- /dev/null +++ b/oryx/swaggerx/error.go @@ -0,0 +1,35 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package swaggerx + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + "github.com/go-openapi/runtime" +) + +func FormatSwaggerError(err error) string { + var e *runtime.APIError + if errors.As(err, &e) { + body, err := json.MarshalIndent(e, "\t", " ") + if err != nil { + body = []byte(fmt.Sprintf("%+v", e.Response)) + } + + switch e.Code { + case http.StatusForbidden: + return fmt.Sprintf("The service responded with status code 403 indicating that you lack permission to access the resource. The full error details are:\n\n\t%s\n\n", body) + case http.StatusUnauthorized: + return fmt.Sprintf("The service responded with status code 401 indicating that you forgot to include credentials (e.g. token, TLS certificate, ...) in the HTTP request. The full error details are:\n\n\t%s\n\n", body) + case http.StatusNotFound: + return fmt.Sprintf("The service responded with status code 404 indicating that the resource does not exist. Check that the URL is correct (are you using the correct admin/public/... endpoint?) and that the resource exists. The full error details are:\n\n\t%s\n\n", body) + default: + return fmt.Sprintf("Unable to complete operation %s because the server responded with status code %d:\n\n\t%s\n", e.OperationName, e.Code, body) + } + } + return fmt.Sprintf("%+v", err) +} diff --git a/oryx/testingx/helpers.go b/oryx/testingx/helpers.go new file mode 100644 index 0000000..a778efd --- /dev/null +++ b/oryx/testingx/helpers.go @@ -0,0 +1,27 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Package testingx contains helper functions and extensions used when writing tests in Ory. +package testingx + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/stretchr/testify/require" +) + +// RepoRootPath returns the absolute path of the closest parent directory that has a go.mod file relative to the caller. +func RepoRootPath(t require.TestingT) (repoRoot string) { + _, fpath, _, _ := runtime.Caller(1) + for dir := filepath.Dir(filepath.FromSlash(fpath)); dir != filepath.Dir(dir); dir = filepath.Dir(dir) { + modPath := filepath.Join(dir, "go.mod") + if _, err := os.Stat(modPath); err == nil { + repoRoot = dir + break + } + } + require.NotEmptyf(t, repoRoot, "could not determine repo root using path: %q", fpath) + return repoRoot +} diff --git a/oryx/tlsx/cert.go b/oryx/tlsx/cert.go new file mode 100644 index 0000000..aa0a28f --- /dev/null +++ b/oryx/tlsx/cert.go @@ -0,0 +1,401 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tlsx + +import ( + "bytes" + "context" + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "math/big" + "os" + "path/filepath" + "slices" + "sync/atomic" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + "github.com/ory/x/watcherx" +) + +// ErrNoCertificatesConfigured is returned when no TLS configuration was found. +var ErrNoCertificatesConfigured = errors.New("no tls configuration was found") + +// ErrInvalidCertificateConfiguration is returned when an invalid TLS configuration was found. +var ErrInvalidCertificateConfiguration = errors.New("tls configuration is invalid") + +// HTTPSCertificate returns loads a HTTP over TLS Certificate by looking at environment variables. +func HTTPSCertificate() ([]tls.Certificate, error) { + prefix := "HTTPS_TLS" + return Certificate( + os.Getenv(prefix+"_CERT"), os.Getenv(prefix+"_KEY"), + os.Getenv(prefix+"_CERT_PATH"), os.Getenv(prefix+"_KEY_PATH"), + ) +} + +// HTTPSCertificateHelpMessage returns a help message for configuring HTTP over TLS Certificates. +func HTTPSCertificateHelpMessage() string { + return CertificateHelpMessage("HTTPS_TLS") +} + +// CertificateHelpMessage returns a help message for configuring TLS Certificates. +func CertificateHelpMessage(prefix string) string { + return `- ` + prefix + `_CERT_PATH: The path to the TLS certificate (pem encoded). + Example: ` + prefix + `_CERT_PATH=~/cert.pem + +- ` + prefix + `_KEY_PATH: The path to the TLS private key (pem encoded). + Example: ` + prefix + `_KEY_PATH=~/key.pem + +- ` + prefix + `_CERT: Base64 encoded (without padding) string of the TLS certificate (PEM encoded) to be used for HTTP over TLS (HTTPS). + Example: ` + prefix + `_CERT="-----BEGIN CERTIFICATE-----\nMIIDZTCCAk2gAwIBAgIEV5xOtDANBgkqhkiG9w0BAQ0FADA0MTIwMAYDVQQDDClP..." + +- ` + prefix + `_KEY: Base64 encoded (without padding) string of the private key (PEM encoded) to be used for HTTP over TLS (HTTPS). + Example: ` + prefix + `_KEY="-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFDjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDg..." +` +} + +// CertificateFromBase64 loads a TLS certificate from a base64-encoded string of +// the PEM representations of the cert and key. +func CertificateFromBase64(certBase64, keyBase64 string) (tls.Certificate, error) { + certPEM, err := base64.StdEncoding.DecodeString(certBase64) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to base64 decode the TLS certificate: %v", err) + } + keyPEM, err := base64.StdEncoding.DecodeString(keyBase64) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to base64 decode the TLS private key: %v", err) + } + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return tls.Certificate{}, fmt.Errorf("unable to load X509 key pair: %v", err) + } + return cert, nil +} + +// [deprecated] Certificate returns a TLS Certificate by looking at its +// arguments. If both certPEMBase64 and keyPEMBase64 are not empty and contain +// base64-encoded PEM representations of a cert and key, respectively, that key +// pair is returned. Otherwise, if certPath and keyPath point to PEM files, the +// key pair is loaded from those. Returns ErrNoCertificatesConfigured if all +// arguments are empty, and ErrInvalidCertificateConfiguration if the arguments +// are inconsistent. +// +// This function is deprecated. Use CertificateFromBase64 or GetCertificate +// instead. +func Certificate( + certPEMBase64, keyPEMBase64 string, + certPath, keyPath string, +) ([]tls.Certificate, error) { + if certPEMBase64 == "" && keyPEMBase64 == "" && certPath == "" && keyPath == "" { + return nil, errors.WithStack(ErrNoCertificatesConfigured) + } + + if certPEMBase64 != "" && keyPEMBase64 != "" { + cert, err := CertificateFromBase64(certPEMBase64, keyPEMBase64) + if err != nil { + return nil, errors.WithStack(err) + } + return []tls.Certificate{cert}, nil + } + + if certPath != "" && keyPath != "" { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, fmt.Errorf("unable to load X509 key pair from files: %v", err) + } + return []tls.Certificate{cert}, nil + } + + return nil, errors.WithStack(ErrInvalidCertificateConfiguration) +} + +type CertFunc = func(*tls.ClientHelloInfo) (*tls.Certificate, error) + +// GetCertificate returns a function for use with +// "net/tls".Config.GetCertificate. +// +// The certificate and private key are read from the specified filesystem paths. +// The certificate file is watched for changes, upon which the cert+key are +// reloaded in the background. Errors during reloading are deduplicated and +// reported through the errs channel if it is not nil. When the provided context +// is canceled, background reloading stops and the errs channel is closed. +// +// The returned function always yields the latest successfully loaded +// certificate; ClientHelloInfo is unused. +func GetCertificate( + ctx context.Context, + certPath, keyPath string, + errs chan<- error, +) (CertFunc, error) { + if certPath == "" || keyPath == "" { + return nil, errors.WithStack(ErrNoCertificatesConfigured) + } + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, errors.WithStack(fmt.Errorf("unable to load X509 key pair from files: %v", err)) + } + var store atomic.Value + store.Store(&cert) + + events := make(chan watcherx.Event) + // The cert could change without the key changing, but not the other way around. + // Hence, we only watch the cert. + _, err = watcherx.WatchFile(ctx, certPath, events) + if err != nil { + return nil, errors.WithStack(err) + } + go func() { + if errs != nil { + defer close(errs) + } + var lastReportedError string + for { + select { + case <-ctx.Done(): + return + + case event := <-events: + var err error + switch event := event.(type) { + case *watcherx.ChangeEvent: + var cert tls.Certificate + cert, err = tls.LoadX509KeyPair(certPath, keyPath) + if err == nil { + store.Store(&cert) + lastReportedError = "" + continue + } + err = fmt.Errorf("unable to load X509 key pair from files: %v", err) + + case *watcherx.ErrorEvent: + err = fmt.Errorf("file watch: %v", event) + default: + continue + } + + if err.Error() == lastReportedError { // same message as before: don't spam the error channel + continue + } + // fresh error + select { + case errs <- errors.WithStack(err): + lastReportedError = err.Error() + case <-time.After(500 * time.Millisecond): + } + } + } + }() + + return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { + if cert, ok := store.Load().(*tls.Certificate); ok { + return cert, nil + } + return nil, errors.WithStack(ErrNoCertificatesConfigured) + }, nil +} + +// PublicKey returns the public key for a given private key, or nil. +func PublicKey(key crypto.PrivateKey) interface{ Equal(x crypto.PublicKey) bool } { + switch k := key.(type) { + case *rsa.PrivateKey: + return &k.PublicKey + case *ecdsa.PrivateKey: + return &k.PublicKey + case ed25519.PrivateKey: + return k.Public().(ed25519.PublicKey) + default: + return nil + } +} + +// CreateSelfSignedTLSCertificate creates a self-signed TLS certificate. +func CreateSelfSignedTLSCertificate(key interface{}, opts ...CertificateOpts) (*tls.Certificate, error) { + c, err := CreateSelfSignedCertificate(key, opts...) + if err != nil { + return nil, err + } + + block, err := PEMBlockForKey(key) + if err != nil { + return nil, err + } + + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw}) + pemKey := pem.EncodeToMemory(block) + cert, err := tls.X509KeyPair(pemCert, pemKey) + if err != nil { + return nil, err + } + + return &cert, nil +} + +// CreateSelfSignedCertificate creates a self-signed x509 certificate. +func CreateSelfSignedCertificate(key interface{}, opts ...CertificateOpts) (cert *x509.Certificate, err error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return cert, errors.Errorf("failed to generate serial number: %s", err) + } + + certificate := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"ORY GmbH"}, + CommonName: "ORY", + }, + Issuer: pkix.Name{ + Organization: []string{"ORY GmbH"}, + CommonName: "ORY", + }, + NotBefore: time.Now().UTC(), + NotAfter: time.Now().UTC().Add(time.Hour * 24 * 31), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: true, + DNSNames: []string{"localhost"}, + } + for _, opt := range opts { + opt(certificate) + } + + der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, PublicKey(key), key) + if err != nil { + return cert, errors.Errorf("failed to create certificate: %s", err) + } + + cert, err = x509.ParseCertificate(der) + if err != nil { + return cert, errors.Errorf("failed to encode private key: %s", err) + } + return cert, nil +} + +// PEMBlockForKey returns a PEM-encoded block for key. +func PEMBlockForKey(key interface{}) (*pem.Block, error) { + b, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, errors.WithStack(err) + } + return &pem.Block{Type: "PRIVATE KEY", Bytes: b}, nil +} + +// NewClientCert creates a new client TLS certificate signed by the given CA. +func NewClientCert(CAcert *x509.Certificate, CAkey crypto.PrivateKey, opts ...CertificateOpts) (*tls.Certificate, error) { + if !slices.Contains(CAcert.ExtKeyUsage, x509.ExtKeyUsageClientAuth) { + return nil, errors.Errorf("the CA certificate does not have the client authentication extended key usage (OID 1.3.6.1.5.5.7.3.2) set") + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + return nil, errors.Errorf("failed to generate serial number: %s", err) + } + + key, err := rsa.GenerateKey(rand.Reader, 3072) + if err != nil { + return nil, errors.Errorf("failed to generate private key: %s", err) + } + + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Ory GmbH"}, + CommonName: "ORY", + }, + Issuer: CAcert.Subject, + NotBefore: time.Now().UTC(), + NotAfter: CAcert.NotAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + IsCA: false, + } + for _, opt := range opts { + opt(template) + } + + der, err := x509.CreateCertificate(rand.Reader, template, CAcert, PublicKey(key), CAkey) + if err != nil { + return nil, errors.Errorf("failed to create certificate: %s", err) + } + + pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) + pemBlock, err := PEMBlockForKey(key) + if err != nil { + return nil, err + } + pemKey := pem.EncodeToMemory(pemBlock) + + cert, err := tls.X509KeyPair(pemCert, pemKey) + if err != nil { + return nil, errors.WithStack(err) + } + return &cert, nil +} + +type CertificateOpts func(*x509.Certificate) + +// CreateSelfSignedCertificateForTest writes a new, self-signed TLS +// certificate+key (in PEM format) to a temporary location on disk and returns +// the paths to both, and the respective contents in base64 encoding. The +// files are automatically cleaned up when the given *testing.T concludes its +// tests. +func CreateSelfSignedCertificateForTest(t testing.TB) (certPath, keyPath, certBase64, keyBase64 string) { + tmpDir := t.TempDir() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + cert, err := CreateSelfSignedCertificate(privateKey) + require.NoError(t, err) + + // write cert + certFile, err := os.Create(filepath.Join(tmpDir, "cert.pem")) + require.NoError(t, err) + certPath = certFile.Name() + + var buf bytes.Buffer + enc := base64.NewEncoder(base64.StdEncoding, &buf) + require.NoErrorf(t, pem.Encode( + io.MultiWriter(enc, certFile), + &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}, + ), "Failed to write data to %q", certPath) + require.NoError(t, enc.Close()) + require.NoErrorf(t, certFile.Close(), "Error closing %q", certPath) + certBase64 = buf.String() + + // write key + keyFile, err := os.Create(filepath.Join(tmpDir, "key.pem")) + require.NoError(t, err) + keyPath = keyFile.Name() + buf.Reset() + enc = base64.NewEncoder(base64.StdEncoding, &buf) + + privBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) + require.NoError(t, err) + + require.NoErrorf(t, pem.Encode( + io.MultiWriter(enc, keyFile), + &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}, + ), "Failed to write data to %q", keyPath) + require.NoError(t, enc.Close()) + require.NoErrorf(t, keyFile.Close(), "Error closing %q", keyPath) + keyBase64 = buf.String() + + return +} diff --git a/oryx/tlsx/termination.go b/oryx/tlsx/termination.go new file mode 100644 index 0000000..7e684a4 --- /dev/null +++ b/oryx/tlsx/termination.go @@ -0,0 +1,95 @@ +// Copyright © 2025 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package tlsx + +import ( + "net" + "net/http" + "strings" + + "github.com/pkg/errors" + "github.com/urfave/negroni" + + "github.com/ory/x/healthx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + "github.com/ory/x/prometheusx" +) + +type dependencies interface { + logrusx.Provider + httpx.WriterProvider +} + +// EnforceTLSRequests creates a middleware that enforces TLS for incoming HTTP requests. +// It allows termination (non-HTTPS traffic) from specific CIDR ranges provided in the `allowTerminationFrom` slice. +// If the request is not secure and does not match the allowed CIDR ranges, an error response is returned. +// The middleware also validates the `X-Forwarded-Proto` header to ensure it is set to "https". +func EnforceTLSRequests(d dependencies, allowTerminationFrom []string) (negroni.Handler, error) { + networks := make([]*net.IPNet, 0, len(allowTerminationFrom)) + for _, rn := range allowTerminationFrom { + _, network, err := net.ParseCIDR(rn) + if err != nil { + return nil, errors.WithStack(err) + } + networks = append(networks, network) + } + + return negroni.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if r.TLS != nil || + r.URL.Path == healthx.AliveCheckPath || + r.URL.Path == healthx.ReadyCheckPath || + r.URL.Path == prometheusx.MetricsPrometheusPath { + next(rw, r) + return + } + + if len(networks) == 0 { + d.Logger().WithRequest(r).WithError(errors.New("TLS termination is not enabled")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } + + if err := matchesRange(r, networks); err != nil { + d.Logger().WithRequest(r).WithError(err).Warnln("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } + + proto := r.Header.Get("X-Forwarded-Proto") + if proto == "" { + d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.New("can not serve request over insecure http")) + return + } else if proto != "https" { + d.Logger().WithRequest(r).WithError(errors.New("X-Forwarded-Proto header is missing")).Error("Could not serve http connection") + d.Writer().WriteErrorCode(rw, r, http.StatusBadGateway, errors.Errorf("expected X-Forwarded-Proto header to be https but got: %s", proto)) + return + } + + next(rw, r) + }), nil +} + +func matchesRange(r *http.Request, networks []*net.IPNet) error { + remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return errors.WithStack(err) + } + + check := []string{remoteIP} + for fwd := range strings.SplitSeq(r.Header.Get("X-Forwarded-For"), ",") { + check = append(check, strings.TrimSpace(fwd)) + } + + for _, ipNet := range networks { + for _, ip := range check { + addr := net.ParseIP(ip) + if ipNet.Contains(addr) { + return nil + } + } + } + return errors.Errorf("neither remote address nor any x-forwarded-for values match CIDR ranges %+v: %v, ranges, check)", networks, check) +} diff --git a/oryx/urlx/copy.go b/oryx/urlx/copy.go new file mode 100644 index 0000000..2e558cb --- /dev/null +++ b/oryx/urlx/copy.go @@ -0,0 +1,24 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import "net/url" + +// Copy returns a copy of the input url. +func Copy(src *url.URL) *url.URL { + var out = new(url.URL) + *out = *src + return out +} + +// CopyWithQuery returns a copy of the input url with the given query parameters +func CopyWithQuery(src *url.URL, query url.Values) *url.URL { + out := Copy(src) + q := out.Query() + for k := range query { + q.Set(k, query.Get(k)) + } + out.RawQuery = q.Encode() + return out +} diff --git a/oryx/urlx/extract.go b/oryx/urlx/extract.go new file mode 100644 index 0000000..6081832 --- /dev/null +++ b/oryx/urlx/extract.go @@ -0,0 +1,139 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "context" + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/pkg/errors" +) + +// hostCache caches DNS lookup results for hostnames to avoid repeated lookups. +// The cache is thread-safe and stores true/false whether a hostname resolves to public IPs. +type hostCache struct { + mu sync.RWMutex + cache map[string]bool +} + +// get retrieves a cached value for a hostname. Returns value and whether it was found. +func (hc *hostCache) get(hostname string) (bool, bool) { + hc.mu.RLock() + defer hc.mu.RUnlock() + isPublic, found := hc.cache[hostname] + return isPublic, found +} + +// set stores the lookup result for a hostname. +func (hc *hostCache) set(hostname string, isPublic bool) { + hc.mu.Lock() + defer hc.mu.Unlock() + hc.cache[hostname] = isPublic +} + +// localCache lives for the lifetime of the main process. The cache +// size is not expected to grow more than a few hundred bytes. +var localCache = &hostCache{ + cache: make(map[string]bool), +} + +// ExtractPublicAddress iterates over parameters and extracts the first public +// address found. Parameter values are assumed to be in priority order. Returns +// an empty string if only private addresses are available. +func ExtractPublicAddress(values ...string) string { + for _, value := range values { + if value == "" || value == "*" { + continue + } + host := value + + // parse URL addresses + if u, err := url.Parse(value); err == nil && len(u.Host) > 1 { + host = removeWildcardsFromHostname(u.Host) + } + + // strip port on both URL and non-URL addresses + hostname, _, err := net.SplitHostPort(host) + if err != nil { + hostname = host + } + + // for IP addresses + if ip := net.ParseIP(hostname); ip != nil { + if !isPrivateIP(ip) { + return host + } + continue + } + + // for hostnames, first check cache + if isPublic, found := localCache.get(hostname); found { + if isPublic { + return host + } + continue + } + + // otherwise, perform DNS lookup & cache result + isPublic := isPublicHostname(hostname) + localCache.set(hostname, isPublic) + if isPublic { + return host + } + } + + return "" +} + +// isPrivateIP checks if an IP address is private (RFC 1918/4193). +func isPrivateIP(ip net.IP) bool { + return ip.IsPrivate() || + ip.IsLoopback() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsUnspecified() // 0.0.0.0 or :: +} + +// isPublicHostname performs DNS lookup to determine if hostname resolves to public IPs. +// Returns true if at least one resolved IP is public, false if all are private or lookup fails. +func isPublicHostname(hostname string) bool { + // avoid DNS lookup if localhost + lower := strings.ToLower(hostname) + if lower == "localhost" { + return false + } + + ctx, cancel := context.WithTimeoutCause(context.Background(), 2*time.Second, errors.Errorf("failed to resolve DNS for %s within 2s", hostname)) + defer cancel() + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) + if err != nil { + return false + } + + for _, ip := range ips { + if !isPrivateIP(ip.IP) { + return true + } + } + + return false +} + +// removeWildcardsFromHostname removes wildcard segments from a hostname string +// by splitting on dots and filtering out asterisk-only segments. +func removeWildcardsFromHostname(hostname string) string { + sep := strings.Split(hostname, ".") + clean := make([]string, 0, len(sep)) + for _, s := range sep { + if s != "*" && s != "" { + clean = append(clean, s) + } + } + return strings.Join(clean, ".") +} diff --git a/oryx/urlx/join.go b/oryx/urlx/join.go new file mode 100644 index 0000000..90eeb15 --- /dev/null +++ b/oryx/urlx/join.go @@ -0,0 +1,52 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "net/url" + "path" + + "github.com/ory/x/cmdx" +) + +// MustJoin joins the paths of two URLs. Fatals if first is not a DSN. +func MustJoin(first string, parts ...string) string { + u, err := url.Parse(first) + if err != nil { + cmdx.Fatalf("Unable to parse %s: %s", first, err) + } + return AppendPaths(u, parts...).String() +} + +// AppendPaths appends the provided paths to the url. +// Paths are intentionally *not* URL encoded. +// The caller is responsible for url encoding, possibly selectively, the required path components with `url.PathEscape`. +func AppendPaths(u *url.URL, paths ...string) (ep *url.URL) { + ep = Copy(u) + if len(paths) == 0 { + return ep + } + + ep.Path = path.Join(append([]string{ep.Path}, paths...)...) + + last := paths[len(paths)-1] + if last != "" && last[len(last)-1] == '/' { + ep.Path = ep.Path + "/" + } + + return ep +} + +// SetQuery appends the provided url values to the DSN's query string. +func SetQuery(u *url.URL, query url.Values) (ep *url.URL) { + ep = Copy(u) + q := ep.Query() + + for k := range query { + q.Set(k, query.Get(k)) + } + + ep.RawQuery = q.Encode() + return ep +} diff --git a/oryx/urlx/parse.go b/oryx/urlx/parse.go new file mode 100644 index 0000000..d6a7706 --- /dev/null +++ b/oryx/urlx/parse.go @@ -0,0 +1,119 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package urlx + +import ( + "net/url" + "regexp" + "strings" + + "github.com/ory/x/logrusx" +) + +// winPathRegex is a regex for [DRIVE-LETTER]: +var winPathRegex = regexp.MustCompile("^[A-Za-z]:.*") + +// Parse parses rawURL into a URL structure with special handling for file:// URLs +// +// File URLs with relative paths (file://../file, ../file) will be returned as a +// url.URL object without the Scheme set to "file". This is because the file +// scheme does not support relative paths. Make sure to check for +// both "file" or "" (an empty string) in URL.Scheme if you are looking for +// a file path. +// +// Use the companion function GetURLFilePath() to get a file path suitable +// for the current operating system. +func Parse(rawURL string) (*url.URL, error) { + lcRawURL := strings.ToLower(rawURL) + if strings.HasPrefix(lcRawURL, "file:///") { + return url.Parse(rawURL) + } + + // Normally the first part after file:// is a hostname, but since + // this is often misused we interpret the URL like a normal path + // by removing the "file://" from the beginning (if it exists) + rawURL = trimPrefixIC(rawURL, "file://") + + if winPathRegex.MatchString(rawURL) { + // Windows path + return url.Parse("file:///" + rawURL) + } + + if strings.HasPrefix(lcRawURL, "\\\\") { + // Windows UNC path + // We extract the hostname and create an appropriate file:// URL + // based on the hostname and the path + host, path := extractUNCPathParts(rawURL) + // It is safe to replace the \ with / here because this is POSIX style path + return url.Parse("file://" + host + strings.ReplaceAll(path, "\\", "/")) + } + + parsed, err := url.Parse(rawURL) + if err != nil { + return nil, err + } + // Since go1.19: + // + // > The URL type now distinguishes between URLs with no authority and URLs with an empty authority. + // > For example, http:///path has an empty authority (host), while http:/path has none. + // + // See https://golang.org/doc/go1.19#net/url for more details. + parsed.OmitHost = false + return parsed, nil +} + +// ParseOrPanic parses a url or panics. +func ParseOrPanic(in string) *url.URL { + out, err := url.Parse(in) + if err != nil { + panic(err.Error()) + } + return out +} + +// ParseOrFatal parses a url or fatals. +func ParseOrFatal(l *logrusx.Logger, in string) *url.URL { + out, err := url.Parse(in) + if err != nil { + l.WithError(err).Fatalf("Unable to parse url: %s", in) + } + return out +} + +// ParseRequestURIOrPanic parses a request uri or panics. +func ParseRequestURIOrPanic(in string) *url.URL { + out, err := url.ParseRequestURI(in) + if err != nil { + panic(err.Error()) + } + return out +} + +// ParseRequestURIOrFatal parses a request uri or fatals. +func ParseRequestURIOrFatal(l *logrusx.Logger, in string) *url.URL { + out, err := url.ParseRequestURI(in) + if err != nil { + l.WithError(err).Fatalf("Unable to parse url: %s", in) + } + return out +} + +func extractUNCPathParts(uncPath string) (host, path string) { + parts := strings.Split(strings.TrimPrefix(uncPath, "\\\\"), "\\") + host = parts[0] + if len(parts) > 0 { + path = "\\" + strings.Join(parts[1:], "\\") + } + return host, path +} + +// trimPrefixIC returns s without the provided leading prefix string using +// case insensitive matching. +// If s doesn't start with prefix, s is returned unchanged. +func trimPrefixIC(s, prefix string) string { + if strings.HasPrefix(strings.ToLower(s), prefix) { + return s[len(prefix):] + } + return s +} diff --git a/oryx/urlx/path.go b/oryx/urlx/path.go new file mode 100644 index 0000000..8ceaddc --- /dev/null +++ b/oryx/urlx/path.go @@ -0,0 +1,19 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build !windows +// +build !windows + +package urlx + +import ( + "net/url" +) + +// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem +func GetURLFilePath(u *url.URL) string { + if u == nil { + return "" + } + return u.Path +} diff --git a/oryx/urlx/path_windows.go b/oryx/urlx/path_windows.go new file mode 100644 index 0000000..cf100d2 --- /dev/null +++ b/oryx/urlx/path_windows.go @@ -0,0 +1,37 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +//go:build windows +// +build windows + +package urlx + +import ( + "net/url" + "path/filepath" + "strings" +) + +// GetURLFilePath returns the path of a URL that is compatible with the runtime os filesystem +func GetURLFilePath(u *url.URL) string { + if u == nil { + return "" + } + if !(u.Scheme == "file" || u.Scheme == "") { + return u.Path + } + + fPath := u.Path + if u.Host != "" { + // Make UNC Path + fPath = "\\\\" + u.Host + filepath.FromSlash(fPath) + return fPath + } + fPathTrimmed := strings.TrimLeft(fPath, "/") + if winPathRegex.MatchString(fPathTrimmed) { + // On Windows we should remove the initial path separator in case this + // is a normal path (for example: "\c:\" -> "c:\"") + fPath = fPathTrimmed + } + return filepath.FromSlash(fPath) +} diff --git a/oryx/uuidx/uuid.go b/oryx/uuidx/uuid.go new file mode 100644 index 0000000..e405746 --- /dev/null +++ b/oryx/uuidx/uuid.go @@ -0,0 +1,11 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package uuidx + +import "github.com/gofrs/uuid" + +// NewV4 returns a new randomly generated UUID or panics. +func NewV4() uuid.UUID { + return uuid.Must(uuid.NewV4()) +} diff --git a/oryx/watcherx/definitions.go b/oryx/watcherx/definitions.go new file mode 100644 index 0000000..ff94079 --- /dev/null +++ b/oryx/watcherx/definitions.go @@ -0,0 +1,78 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "fmt" + "net/url" + + "github.com/pkg/errors" +) + +type ( + errSchemeUnknown struct { + scheme string + } + EventChannel chan Event + Watcher interface { + // DispatchNow fires the watcher and causes an event. + // + // WARNING: The returned channel must be read or no further events will + // be propagated due to a deadlock. + DispatchNow() (<-chan int, error) + } + dispatcher struct { + ctx context.Context + trigger chan struct{} + done chan int + } +) + +var ( + // ErrSchemeUnknown is just for checking with errors.Is() + ErrSchemeUnknown = &errSchemeUnknown{} + ErrWatcherNotRunning = fmt.Errorf("watcher is not running") +) + +func (e *errSchemeUnknown) Is(other error) bool { + _, ok := other.(*errSchemeUnknown) + return ok +} + +func (e *errSchemeUnknown) Error() string { + return fmt.Sprintf("unknown scheme '%s' to watch", e.scheme) +} + +func newDispatcher(ctx context.Context) *dispatcher { + return &dispatcher{ + ctx: ctx, + trigger: make(chan struct{}), + done: make(chan int), + } +} + +func (d *dispatcher) DispatchNow() (<-chan int, error) { + if d.trigger == nil { + return nil, ErrWatcherNotRunning + } + // The trigger send must respect cancellation. Once the watcher's context is + // done its receiver goroutine has returned, so a bare send would block + // forever and wedge the caller. + select { + case d.trigger <- struct{}{}: + return d.done, nil + case <-d.ctx.Done(): + return nil, errors.WithStack(ErrWatcherNotRunning) + } +} + +func Watch(ctx context.Context, u *url.URL, c EventChannel) (Watcher, error) { + switch u.Scheme { + // see urlx.Parse for why the empty string is also file + case "file", "": + return WatchFile(ctx, u.Path, c) + } + return nil, &errSchemeUnknown{u.Scheme} +} diff --git a/oryx/watcherx/directory.go b/oryx/watcherx/directory.go new file mode 100644 index 0000000..f65d0b5 --- /dev/null +++ b/oryx/watcherx/directory.go @@ -0,0 +1,173 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "os" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" +) + +func WatchDirectory(ctx context.Context, dir string, c EventChannel) (Watcher, error) { + w, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithStack(err) + } + subDirs := make(map[string]struct{}) + if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return errors.WithStack(err) + } + if info.IsDir() { + if err := w.Add(path); err != nil { + return errors.WithStack(err) + } + subDirs[path] = struct{}{} + } + return nil + }); err != nil { + return nil, err + } + + dw := &directoryWatcher{ + dispatcher: newDispatcher(ctx), + c: c, + dir: dir, + subDirs: subDirs, + w: w, + } + go dw.streamDirectoryEvents(ctx) + return dw, nil +} + +type directoryWatcher struct { + *dispatcher + c EventChannel + dir string + // subDirsMtx guards subDirs, which is read by the watcher goroutine and + // potentially by external callers (e.g. tests) concurrently. + subDirsMtx sync.RWMutex + subDirs map[string]struct{} + w *fsnotify.Watcher +} + +func (w *directoryWatcher) handleEvent(ctx context.Context, e fsnotify.Event) { + if e.Has(fsnotify.Remove) { + w.subDirsMtx.Lock() + _, isSubDir := w.subDirs[e.Name] + if isSubDir { + // we do not want any event on deletion of a directory + delete(w.subDirs, e.Name) + } + w.subDirsMtx.Unlock() + if isSubDir { + return + } + w.maybeSend(ctx, &RemoveEvent{ + source: source(e.Name), + }) + return + } else if e.Has(fsnotify.Write | fsnotify.Create) { + if stats, err := os.Stat(e.Name); err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: errors.WithStack(err), + source: source(e.Name), + }) + return + } else if stats.IsDir() { + if err := w.w.Add(e.Name); err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: errors.WithStack(err), + source: source(e.Name), + }) + } + w.subDirsMtx.Lock() + w.subDirs[e.Name] = struct{}{} + w.subDirsMtx.Unlock() + return + } + + //#nosec G304 -- false positive + data, err := os.ReadFile(e.Name) + if err != nil { + w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(e.Name), + }) + } else { + w.maybeSend(ctx, &ChangeEvent{ + data: data, + source: source(e.Name), + }) + } + } +} + +func (w *directoryWatcher) maybeSend(ctx context.Context, e Event) bool { + select { + case <-ctx.Done(): + return false + case w.c <- e: + return true + } +} + +func (w *directoryWatcher) streamDirectoryEvents(ctx context.Context) { + defer func() { + close(w.done) + close(w.c) + _ = w.w.Close() + }() + for { + select { + case <-ctx.Done(): + return + case e := <-w.w.Events: + w.handleEvent(ctx, e) + case <-w.trigger: + var eventsSent int + + if err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + //#nosec G304 -- false positive + data, err := os.ReadFile(path) + if err != nil { + if !w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(path), + }) { + return errors.WithStack(context.Canceled) + } + } else { + if !w.maybeSend(ctx, &ChangeEvent{ + data: data, + source: source(path), + }) { + return errors.WithStack(context.Canceled) + } + } + eventsSent++ + } + return nil + }); err != nil { + if !w.maybeSend(ctx, &ErrorEvent{ + error: err, + source: source(w.dir), + }) { + return + } + eventsSent++ + } + + w.done <- eventsSent + } + } +} diff --git a/oryx/watcherx/event.go b/oryx/watcherx/event.go new file mode 100644 index 0000000..a4065c0 --- /dev/null +++ b/oryx/watcherx/event.go @@ -0,0 +1,109 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "bytes" + "encoding/json" + "fmt" + "io" +) + +type ( + Event interface { + // MarshalJSON is required to work multiple times + json.Marshaler + + Reader() io.Reader + Source() string + String() string + setSource(string) + } + source string + ErrorEvent struct { + error + source + } + ChangeEvent struct { + data []byte + source + } + RemoveEvent struct { + source + } + serialEventType string + serialEvent struct { + Type serialEventType `json:"type"` + Data []byte `json:"data"` + Source source `json:"source"` + } +) + +func NewErrorEvent(err error, source_ string) *ErrorEvent { + return &ErrorEvent{ + error: err, + source: source(source_), + } +} + +const ( + serialTypeChange serialEventType = "change" + serialTypeRemove serialEventType = "remove" + serialTypeError serialEventType = "error" +) + +func (e *ErrorEvent) Reader() io.Reader { + return bytes.NewBufferString(e.Error()) +} + +func (e *ErrorEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeError, + Data: []byte(e.Error()), + Source: e.source, + }) +} + +func (e *ErrorEvent) String() string { + return fmt.Sprintf("error: %+v; source: %s", e.error, e.source) +} + +func (e source) Source() string { + return string(e) +} + +func (e *source) setSource(nsrc string) { + *e = source(nsrc) +} + +func (e *ChangeEvent) Reader() io.Reader { + return bytes.NewBuffer(e.data) +} + +func (e *ChangeEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeChange, + Data: e.data, + Source: e.source, + }) +} + +func (e *ChangeEvent) String() string { + return fmt.Sprintf("data: %s; source: %s", e.data, e.source) +} + +func (e *RemoveEvent) Reader() io.Reader { + return nil +} + +func (e *RemoveEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(serialEvent{ + Type: serialTypeRemove, + Source: e.source, + }) +} + +func (e *RemoveEvent) String() string { + return fmt.Sprintf("removed source: %s", e.source) +} diff --git a/oryx/watcherx/file.go b/oryx/watcherx/file.go new file mode 100644 index 0000000..ecb4419 --- /dev/null +++ b/oryx/watcherx/file.go @@ -0,0 +1,174 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "context" + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/pkg/errors" +) + +// WatchFile spawns a background goroutine to watch file, reporting any changes +// to c. Watching stops when ctx is canceled. +func WatchFile(ctx context.Context, file string, c EventChannel) (Watcher, error) { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, errors.WithStack(err) + } + dir := filepath.Dir(file) + if err := watcher.Add(dir); err != nil { + return nil, errors.WithStack(err) + } + resolvedFile, err := filepath.EvalSymlinks(file) + if err != nil { + if pathError := new(os.PathError); !errors.As(err, &pathError) { + return nil, errors.WithStack(err) + } + // The file does not exist. The watcher should still watch the directory + // to get notified about file creation. + resolvedFile = "" + } else if resolvedFile != file { + // If `resolvedFile` != `file` then `file` is a symlink and we have to explicitly watch the referenced file. + // This is because fsnotify follows symlinks and watches the destination file, not the symlink + // itself. That is at least the case for unix systems. See: https://github.com/fsnotify/fsnotify/issues/199 + if err := watcher.Add(file); err != nil { + return nil, errors.WithStack(err) + } + } + d := newDispatcher(ctx) + go streamFileEvents(ctx, watcher, c, d.trigger, d.done, file, resolvedFile) + return d, nil +} + +// streamFileEvents watches for file changes and supports symlinks which requires several workarounds due to limitations of fsnotify. +// Argument `resolvedFile` is the resolved symlink path of the file, or it is the watchedFile name itself. If `resolvedFile` is empty, then the watchedFile does not exist. +func streamFileEvents(ctx context.Context, watcher *fsnotify.Watcher, c EventChannel, sendNow <-chan struct{}, sendNowDone chan<- int, watchedFile, resolvedFile string) { + eventSource := source(watchedFile) + removeDirectFileWatcher := func() { + _ = watcher.Remove(watchedFile) + } + addDirectFileWatcher := func() { + // check if the watchedFile (symlink) exists + // if it does not the dir watcher will notify us when it gets created + if _, err := os.Lstat(watchedFile); err == nil { + if err := watcher.Add(watchedFile); err != nil { + c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + } + } + } + } + defer watcher.Close() + for { + select { + case <-ctx.Done(): + return + case <-sendNow: + if resolvedFile == "" { + // The file does not exist. Announce this by sending a RemoveEvent. + c <- &RemoveEvent{eventSource} + } else { + // The file does exist. Announce the current content by sending a ChangeEvent. + // #nosec G304 -- false positive + data, err := os.ReadFile(watchedFile) + if err != nil { + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + select { + case c <- &ChangeEvent{ + data: data, + source: eventSource, + }: + case <-ctx.Done(): + return + } + } + + // in any of the above cases we send exactly one event + select { + case sendNowDone <- 1: + case <-ctx.Done(): + return + } + case e, ok := <-watcher.Events: + if !ok { + return + } + // filter events to only watch watchedFile + // e.Name contains the name of the watchedFile (regardless whether it is a symlink), not the resolved file name + if filepath.Clean(e.Name) == watchedFile { + recentlyResolvedFile, err := filepath.EvalSymlinks(watchedFile) + // when there is no error the file exists and any symlinks can be resolved + if err != nil { + // check if the watchedFile (or the file behind the symlink) was removed + if _, ok := err.(*os.PathError); ok { + select { + case c <- &RemoveEvent{eventSource}: + case <-ctx.Done(): + return + } + removeDirectFileWatcher() + continue + } + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + // This catches following three cases: + // 1. the watchedFile was written or created + // 2. the watchedFile is a symlink and has changed (k8s config map updates) + // 3. the watchedFile behind the symlink was written or created + switch { + case recentlyResolvedFile != resolvedFile: + resolvedFile = recentlyResolvedFile + // watch the symlink again to update the actually watched file + removeDirectFileWatcher() + addDirectFileWatcher() + // we fallthrough because we also want to read the file in this case + fallthrough + case e.Has(fsnotify.Write | fsnotify.Create): + // #nosec G304 -- false positive + data, err := os.ReadFile(watchedFile) + if err != nil { + select { + case c <- &ErrorEvent{ + error: errors.WithStack(err), + source: eventSource, + }: + case <-ctx.Done(): + return + } + continue + } + select { + case c <- &ChangeEvent{ + data: data, + source: eventSource, + }: + case <-ctx.Done(): + return + } + } + } + } + } +} diff --git a/oryx/watcherx/integrationtest/.dockerignore b/oryx/watcherx/integrationtest/.dockerignore new file mode 100644 index 0000000..515faed --- /dev/null +++ b/oryx/watcherx/integrationtest/.dockerignore @@ -0,0 +1,7 @@ +event_logger.yml +configmap.yml + +Makefile +README.md + +eventlog_snapshot diff --git a/oryx/watcherx/integrationtest/.gitignore b/oryx/watcherx/integrationtest/.gitignore new file mode 100644 index 0000000..2533ea2 --- /dev/null +++ b/oryx/watcherx/integrationtest/.gitignore @@ -0,0 +1 @@ +tmp_snapshot diff --git a/oryx/watcherx/integrationtest/Dockerfile b/oryx/watcherx/integrationtest/Dockerfile new file mode 100644 index 0000000..07b9713 --- /dev/null +++ b/oryx/watcherx/integrationtest/Dockerfile @@ -0,0 +1,21 @@ +FROM golang:1.26-alpine3.23 AS builder + +RUN apk -U --no-cache add build-base + +WORKDIR /go/src/github.com/ory/x + +ADD go.mod go.mod +ADD go.sum go.sum + +RUN go mod download + +ADD . . + +RUN go build -o /usr/bin/eventlogger ./watcherx/integrationtest + +FROM alpine:3.22 + +COPY --from=builder /usr/bin/eventlogger /usr/bin/eventlogger + +ENTRYPOINT ["eventlogger"] +CMD ["/etc/config/mock-config"] diff --git a/oryx/watcherx/integrationtest/Makefile b/oryx/watcherx/integrationtest/Makefile new file mode 100644 index 0000000..7725e79 --- /dev/null +++ b/oryx/watcherx/integrationtest/Makefile @@ -0,0 +1,65 @@ +SHELL=/bin/bash -euo pipefail + +CLUSTER_NAME=watcherx-integration-test +SNAPSHOT_FILE=eventlog_snapshot + +define generate_snapshot + sleep 5 + make update + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) + make apply + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) + make update + sleep 1 + kubectl logs eventlogger --context kind-${CLUSTER_NAME} >> $(1) +endef + +.PHONY: build +build: + docker build -f Dockerfile -t eventlogger:latest ../.. + +.PHONY: create +create: + kind create cluster --name ${CLUSTER_NAME} --wait 1m || true + +.PHONY: load +load: + kind load docker-image eventlogger:latest --name ${CLUSTER_NAME} + +.PHONY: apply +apply: + kubectl apply -f configmap.yml -f event_logger.yml --context kind-${CLUSTER_NAME} + +.PHONY: delete +delete: + kind delete cluster --name ${CLUSTER_NAME} + +.PHONY: setup +setup: build create load apply + +.PHONY: snapshot +snapshot: setup container-restart + rm ${SNAPSHOT_FILE} + ${call generate_snapshot,$(SNAPSHOT_FILE)} + +.PHONY: check +check: setup container-restart + rm tmp_snapshot || true + ${call generate_snapshot,tmp_snapshot} + diff tmp_snapshot ${SNAPSHOT_FILE} + +.PHONY: logs +logs: + kubectl logs eventlogger --context kind-${CLUSTER_NAME} + +.PHONY: container-restart +container-restart: + kubectl delete -f event_logger.yml --context kind-${CLUSTER_NAME} + kubectl apply -f event_logger.yml --context kind-${CLUSTER_NAME} + +.PHONY: update +update: + cat configmap.yml | sed 's/somevalue/othervalue/' | kubectl apply -f - --context kind-${CLUSTER_NAME} + cat event_logger.yml | sed 's/somevalue/othervalue/' | kubectl apply -f - --context kind-${CLUSTER_NAME} diff --git a/oryx/watcherx/integrationtest/README.md b/oryx/watcherx/integrationtest/README.md new file mode 100644 index 0000000..4dedd3b --- /dev/null +++ b/oryx/watcherx/integrationtest/README.md @@ -0,0 +1,27 @@ +# Integration Test for watcherx/FileWatcher + +As kubernetes has a special way to change mounted config map values we want to +make sure our file watcher is compatible with that. + +## Perquisites + +The versions are the ones that definitely work. + +- kind (v0.8.1) +- kubectl (v1.18.5) +- docker (v19.03.12-ce) +- make (v4.3) + +## Structure + +The `main.go` just logs all events it gets. It is deployed to a kind kubernetes +cluster together with a configmap that gets updated during the test. For details +on the test steps have a look at the `Makefile`. + +## Running + +To generate the log snapshot run `make snapshot`. That snapshot should be +committed. To check if the FileWatcher works run `make check`. For debugging +purposes single steps of the setup have descriptive make target names and can be +run separately. It is safe to delete the cluster at any point or rerun snapshot +generation. diff --git a/oryx/watcherx/integrationtest/configmap.yml b/oryx/watcherx/integrationtest/configmap.yml new file mode 100644 index 0000000..b8c5cee --- /dev/null +++ b/oryx/watcherx/integrationtest/configmap.yml @@ -0,0 +1,6 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: changing-config +data: + mock-config: somevalue diff --git a/oryx/watcherx/integrationtest/event_logger.yml b/oryx/watcherx/integrationtest/event_logger.yml new file mode 100644 index 0000000..acd9641 --- /dev/null +++ b/oryx/watcherx/integrationtest/event_logger.yml @@ -0,0 +1,19 @@ +kind: Pod +apiVersion: v1 +metadata: + name: eventlogger + annotations: + variant: somevalue +spec: + containers: + - name: eventlogger + image: eventlogger:latest + imagePullPolicy: Never + volumeMounts: + - name: changing-config + mountPath: /etc/config + restartPolicy: Never + volumes: + - name: changing-config + configMap: + name: changing-config diff --git a/oryx/watcherx/integrationtest/eventlog_snapshot b/oryx/watcherx/integrationtest/eventlog_snapshot new file mode 100644 index 0000000..c9ab6a7 --- /dev/null +++ b/oryx/watcherx/integrationtest/eventlog_snapshot @@ -0,0 +1,21 @@ +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +got change event: +Data: somevalue, +Src: /etc/config/mock-config +watching file /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config +got change event: +Data: somevalue, +Src: /etc/config/mock-config +got change event: +Data: othervalue, +Src: /etc/config/mock-config diff --git a/oryx/watcherx/integrationtest/main.go b/oryx/watcherx/integrationtest/main.go new file mode 100644 index 0000000..b66085b --- /dev/null +++ b/oryx/watcherx/integrationtest/main.go @@ -0,0 +1,45 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/ory/x/watcherx" +) + +func main() { + if len(os.Args) != 2 { + _, _ = fmt.Fprintf(os.Stderr, "expected 1 comand line argument but got %d\n", len(os.Args)-1) + os.Exit(1) + } + c := make(chan watcherx.Event) + _, err := watcherx.WatchFile(context.Background(), os.Args[1], c) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "could not initialize file watcher: %+v\n", err) + os.Exit(1) + } + fmt.Printf("watching file %s\n", os.Args[1]) + for { + switch e := (<-c).(type) { + case *watcherx.ChangeEvent: + var data []byte + data, err = io.ReadAll(e.Reader()) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "could not read data: %+v\n", err) + os.Exit(1) + } + fmt.Printf("got change event:\nData: %s,\nSrc: %s\n", data, e.Source()) + case *watcherx.RemoveEvent: + fmt.Printf("got remove event:\nSrc: %s\n", e.Source()) + case *watcherx.ErrorEvent: + fmt.Printf("got error event:\nError: %s\n", e.Error()) + default: + fmt.Println("got unknown event") + } + } +} diff --git a/oryx/watcherx/test_helpers.go b/oryx/watcherx/test_helpers.go new file mode 100644 index 0000000..8960458 --- /dev/null +++ b/oryx/watcherx/test_helpers.go @@ -0,0 +1,80 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package watcherx + +import ( + "os" + "path" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func KubernetesAtomicWrite(t *testing.T, dir, fileName, content string) { + // atomic write according to https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/util/atomic_writer.go + const ( + dataDirName = "..data" + newDataDirName = "..data_tmp" + ) + // (2) + dataDirPath := filepath.Join(dir, dataDirName) + oldTsDir, err := os.Readlink(dataDirPath) + if err != nil { + require.True(t, os.IsNotExist(err), "%+v", err) + // although Readlink() returns "" on err, don't be fragile by relying on it (since it's not specified in docs) + // empty oldTsDir indicates that it didn't exist + oldTsDir = "" + } + oldTsPath := filepath.Join(dir, oldTsDir) + + // (3) we are not interested in the case where a file gets deleted as we just operate on one file + // (4) we assume the file needs an update + + // (5) + tsDir, err := os.MkdirTemp(dir, time.Now().UTC().Format("..2006_01_02_15_04_05.")) + require.NoError(t, err) + tsDirName := filepath.Base(tsDir) + + // (6) + require.NoError( + t, + os.WriteFile(path.Join(tsDir, fileName), []byte(content), 0600), + ) + + // (7) + _, err = os.Readlink(filepath.Join(dir, fileName)) + if err != nil && os.IsNotExist(err) { + // The link into the data directory for this path doesn't exist; create it + require.NoError( + t, + os.Symlink(filepath.Join(dataDirName, fileName), filepath.Join(dir, fileName)), + ) + } + + // (8) + newDataDirPath := filepath.Join(dir, newDataDirName) + require.NoError( + t, + os.Symlink(tsDirName, newDataDirPath), + ) + + // (9) + if runtime.GOOS == "windows" { + require.NoError(t, os.Remove(dataDirPath)) + require.NoError(t, os.Symlink(tsDirName, dataDirPath)) + require.NoError(t, os.Remove(newDataDirPath)) + } else { + require.NoError(t, os.Rename(newDataDirPath, dataDirPath)) + } + + // (10) in our case there is nothing to remove + + // (11) + if len(oldTsDir) > 0 { + require.NoError(t, os.RemoveAll(oldTsPath)) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..75bdb59 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "@oryd/keto", + "version": "0.0.0", + "private": true, + "license": "Apache 2.0", + "browser": { + "fs": false + }, + "scripts": { + "doctoc": "doctoc README.md", + "openapi-generator-cli": "openapi-generator-cli" + }, + "prettier": { + "proseWrap": "always", + "semi": false, + "trailingComma": "all" + }, + "devDependencies": { + "@bufbuild/protoc-gen-es": "2.11.0", + "@connectrpc/protoc-gen-connect-es": "1.7.0", + "@openapitools/openapi-generator-cli": "2.26.0", + "@ory/keto-namespace-types": "file:contrib/namespace-type-lib", + "doctoc": "2.4.0", + "license-checker": "25.0.1", + "prettier": "3.8.2", + "prettier-plugin-packagejson": "3.0.2" + }, + "overrides": { + "@bufbuild/protoc-gen-es": "2.11.0", + "axios": ">=1.15.2", + "glob": ">=11.1.0", + "tar": ">=7.5.10", + "grpc_tools_node_protoc_ts": { + "handlebars": "4.7.9" + }, + "shell-quote": "1.9.0", + "@nestjs/core": "11.1.19" + } +} diff --git a/proto/README.md b/proto/README.md new file mode 100644 index 0000000..8e61465 --- /dev/null +++ b/proto/README.md @@ -0,0 +1,10 @@ +# Ory Keto gRPC Client + +This package provides the generated gRPC client for +[Ory Keto](https://ory.com/keto). Go to +[the documentation](https://ory.com/keto/docs) to learn more + +The protocol buffer compiler, `protoc`, is used to compile `.proto` files, which +contain service and message definitions. Go to +[the gRPC documentation](https://grpc.io/docs/protoc-installation/#install-pre-compiled-binaries-any-os) +for installation instructions. diff --git a/proto/go.mod b/proto/go.mod new file mode 100644 index 0000000..4a0743e --- /dev/null +++ b/proto/go.mod @@ -0,0 +1,25 @@ +// Deprecated: This module is replaced by github.com/ory/keto/gen/go. Please update your imports to use the new module path. +module github.com/ory/keto/proto + +go 1.26 + +require ( + github.com/stretchr/testify v1.10.0 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.39.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +retract v0.9.0-alpha.0.pre.0 diff --git a/proto/go.sum b/proto/go.sum new file mode 100644 index 0000000..6af7556 --- /dev/null +++ b/proto/go.sum @@ -0,0 +1,61 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/proto/markdown.tmpl b/proto/markdown.tmpl new file mode 100644 index 0000000..5820e87 --- /dev/null +++ b/proto/markdown.tmpl @@ -0,0 +1,107 @@ + + + +# Protocol Documentation +
+ +## Table of Contents +{{range .Files}} +{{$file_name := .Name}}- [{{.Name}}](#{{.Name | anchor}}) + {{- if .Messages }} + {{range .Messages}} - [{{.LongName}}](#{{.FullName | anchor}}) + {{end}} + {{- end -}} + {{- if .Enums }} + {{range .Enums}} - [{{.LongName}}](#{{.FullName | anchor}}) + {{end}} + {{- end -}} + {{- if .Extensions }} + {{range .Extensions}} - [File-level Extensions](#{{$file_name | anchor}}-extensions) + {{end}} + {{- end -}} + {{- if .Services }} + {{range .Services}} - [{{.Name}}](#{{.FullName | anchor}}) + {{end}} + {{- end -}} +{{end}} +- [Scalar Value Types](#scalar-value-types) + +{{range .Files}} +{{$file_name := .Name}} + +

Top

+ +## {{.Name}} +{{.Description}} + +{{range .Messages}} + + +### {{.LongName}} +{{.Description}} + +{{if .HasFields}} +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +{{range .Fields -}} + | {{.Name}} | [{{.LongType}}](#{{.FullType | anchor}}) | {{.Label}} | {{if (index .Options "deprecated"|default false)}}**Deprecated.** {{end}}{{.Description | replace "\n" "
" | nobr}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}} | +{{end}} +{{end}} + +{{if .HasExtensions}} +| Extension | Type | Base | Number | Description | +| --------- | ---- | ---- | ------ | ----------- | +{{range .Extensions -}} + | {{.Name}} | {{.LongType}} | {{.ContainingLongType}} | {{.Number}} | {{.Description | replace "\n" "
" | nobr}}{{if .DefaultValue}} Default: {{.DefaultValue}}{{end}} | +{{end}} +{{end}} + +{{end}} + +{{range .Enums}} + + +### {{.LongName}} +{{.Description}} + +| Name | Number | Description | +| ---- | ------ | ----------- | +{{range .Values -}} + | {{.Name}} | {{.Number}} | {{.Description | replace "\n" "
" | nobr}} | +{{end}} + +{{end}} + +{{if .HasExtensions}} + + +### File-level Extensions +| Extension | Type | Base | Number | Description | +| --------- | ---- | ---- | ------ | ----------- | +{{range .Extensions -}} + | {{.Name}} | {{.LongType}} | {{.ContainingLongType}} | {{.Number}} | {{.Description | replace "\n" "
" | nobr}}{{if .DefaultValue}} Default: `{{.DefaultValue}}`{{end}} | +{{end}} +{{end}} + +{{range .Services}} + + +### {{.Name}} +{{.Description}} + +| Method Name | Request Type | Response Type | Description | +| ----------- | ------------ | ------------- | ------------| +{{range .Methods -}} + | {{.Name}} | [{{.RequestLongType}}](#{{.RequestFullType | anchor}}){{if .RequestStreaming}} stream{{end}} | [{{.ResponseLongType}}](#{{.ResponseFullType | anchor}}){{if .ResponseStreaming}} stream{{end}} | {{.Description | replace "\n" "
" | nobr}} | +{{end}} +{{end}} + +{{end}} + +## Scalar Value Types + +| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby | +| ----------- | ----- | --- | ---- | ------ | -- | -- | --- | ---- | +{{range .Scalars -}} + | {{.ProtoType}} | {{.Notes}} | {{.CppType}} | {{.JavaType}} | {{.PythonType}} | {{.GoType}} | {{.CSharp}} | {{.PhpType}} | {{.RubyType}} | +{{end}} \ No newline at end of file diff --git a/proto/ory/keto/opl/v1alpha1/syntax_service.pb.go b/proto/ory/keto/opl/v1alpha1/syntax_service.pb.go new file mode 100644 index 0000000..22500f5 --- /dev/null +++ b/proto/ory/keto/opl/v1alpha1/syntax_service.pb.go @@ -0,0 +1,299 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/opl/v1alpha1/syntax_service.proto + +package opl + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content []byte `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckRequest) Reset() { + *x = CheckRequest{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckRequest) ProtoMessage() {} + +func (x *CheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead. +func (*CheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{0} +} + +func (x *CheckRequest) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +type CheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + ParseErrors []*ParseError `protobuf:"bytes,1,rep,name=parse_errors,json=parseErrors,proto3" json:"parse_errors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponse) Reset() { + *x = CheckResponse{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponse) ProtoMessage() {} + +func (x *CheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponse.ProtoReflect.Descriptor instead. +func (*CheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckResponse) GetParseErrors() []*ParseError { + if x != nil { + return x.ParseErrors + } + return nil +} + +type ParseError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Start *SourcePosition `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"` + End *SourcePosition `protobuf:"bytes,3,opt,name=end,proto3" json:"end,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParseError) Reset() { + *x = ParseError{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParseError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseError) ProtoMessage() {} + +func (x *ParseError) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseError.ProtoReflect.Descriptor instead. +func (*ParseError) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ParseError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ParseError) GetStart() *SourcePosition { + if x != nil { + return x.Start + } + return nil +} + +func (x *ParseError) GetEnd() *SourcePosition { + if x != nil { + return x.End + } + return nil +} + +type SourcePosition struct { + state protoimpl.MessageState `protogen:"open.v1"` + Line uint32 `protobuf:"varint,1,opt,name=line,proto3" json:"line,omitempty"` + Column uint32 `protobuf:"varint,2,opt,name=column,proto3" json:"column,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourcePosition) Reset() { + *x = SourcePosition{} + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourcePosition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourcePosition) ProtoMessage() {} + +func (x *SourcePosition) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourcePosition.ProtoReflect.Descriptor instead. +func (*SourcePosition) Descriptor() ([]byte, []int) { + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP(), []int{3} +} + +func (x *SourcePosition) GetLine() uint32 { + if x != nil { + return x.Line + } + return 0 +} + +func (x *SourcePosition) GetColumn() uint32 { + if x != nil { + return x.Column + } + return 0 +} + +var File_ory_keto_opl_v1alpha1_syntax_service_proto protoreflect.FileDescriptor + +const file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc = "" + + "\n" + + "*ory/keto/opl/v1alpha1/syntax_service.proto\x12\x15ory.keto.opl.v1alpha1\"(\n" + + "\fCheckRequest\x12\x18\n" + + "\acontent\x18\x01 \x01(\fR\acontent\"U\n" + + "\rCheckResponse\x12D\n" + + "\fparse_errors\x18\x01 \x03(\v2!.ory.keto.opl.v1alpha1.ParseErrorR\vparseErrors\"\x9c\x01\n" + + "\n" + + "ParseError\x12\x18\n" + + "\amessage\x18\x01 \x01(\tR\amessage\x12;\n" + + "\x05start\x18\x02 \x01(\v2%.ory.keto.opl.v1alpha1.SourcePositionR\x05start\x127\n" + + "\x03end\x18\x03 \x01(\v2%.ory.keto.opl.v1alpha1.SourcePositionR\x03end\"<\n" + + "\x0eSourcePosition\x12\x12\n" + + "\x04line\x18\x01 \x01(\rR\x04line\x12\x16\n" + + "\x06column\x18\x02 \x01(\rR\x06column2c\n" + + "\rSyntaxService\x12R\n" + + "\x05Check\x12#.ory.keto.opl.v1alpha1.CheckRequest\x1a$.ory.keto.opl.v1alpha1.CheckResponseB\xdc\x01\n" + + "\x19com.ory.keto.opl.v1alpha1B\x12SyntaxServiceProtoP\x01Z4github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1;opl\xa2\x02\x03OKO\xaa\x02\x15Ory.Keto.Opl.V1alpha1\xca\x02\x15Ory\\Keto\\Opl\\V1alpha1\xe2\x02!Ory\\Keto\\Opl\\V1alpha1\\GPBMetadata\xea\x02\x18Ory::Keto::Opl::V1alpha1b\x06proto3" + +var ( + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescOnce sync.Once + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData []byte +) + +func file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescGZIP() []byte { + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescOnce.Do(func() { + file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc), len(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc))) + }) + return file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDescData +} + +var file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes = []any{ + (*CheckRequest)(nil), // 0: ory.keto.opl.v1alpha1.CheckRequest + (*CheckResponse)(nil), // 1: ory.keto.opl.v1alpha1.CheckResponse + (*ParseError)(nil), // 2: ory.keto.opl.v1alpha1.ParseError + (*SourcePosition)(nil), // 3: ory.keto.opl.v1alpha1.SourcePosition +} +var file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.opl.v1alpha1.CheckResponse.parse_errors:type_name -> ory.keto.opl.v1alpha1.ParseError + 3, // 1: ory.keto.opl.v1alpha1.ParseError.start:type_name -> ory.keto.opl.v1alpha1.SourcePosition + 3, // 2: ory.keto.opl.v1alpha1.ParseError.end:type_name -> ory.keto.opl.v1alpha1.SourcePosition + 0, // 3: ory.keto.opl.v1alpha1.SyntaxService.Check:input_type -> ory.keto.opl.v1alpha1.CheckRequest + 1, // 4: ory.keto.opl.v1alpha1.SyntaxService.Check:output_type -> ory.keto.opl.v1alpha1.CheckResponse + 4, // [4:5] is the sub-list for method output_type + 3, // [3:4] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ory_keto_opl_v1alpha1_syntax_service_proto_init() } +func file_ory_keto_opl_v1alpha1_syntax_service_proto_init() { + if File_ory_keto_opl_v1alpha1_syntax_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc), len(file_ory_keto_opl_v1alpha1_syntax_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes, + DependencyIndexes: file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs, + MessageInfos: file_ory_keto_opl_v1alpha1_syntax_service_proto_msgTypes, + }.Build() + File_ory_keto_opl_v1alpha1_syntax_service_proto = out.File + file_ory_keto_opl_v1alpha1_syntax_service_proto_goTypes = nil + file_ory_keto_opl_v1alpha1_syntax_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/opl/v1alpha1/syntax_service.proto b/proto/ory/keto/opl/v1alpha1/syntax_service.proto new file mode 100644 index 0000000..156f657 --- /dev/null +++ b/proto/ory/keto/opl/v1alpha1/syntax_service.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package ory.keto.opl.v1alpha1; + +// The service that checks the syntax of an OPL file. +service SyntaxService { + // Performs a syntax check request. + rpc Check(CheckRequest) returns (CheckResponse); +} + +message CheckRequest { + bytes content = 1; +} + +message CheckResponse { + repeated ParseError parse_errors = 1; +} + +message ParseError { + string message = 1; + SourcePosition start = 2; + SourcePosition end = 3; +} + +message SourcePosition { + uint32 line = 1; + uint32 column = 2; +} \ No newline at end of file diff --git a/proto/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go b/proto/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go new file mode 100644 index 0000000..d6b8887 --- /dev/null +++ b/proto/ory/keto/opl/v1alpha1/syntax_service_grpc.pb.go @@ -0,0 +1,127 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/opl/v1alpha1/syntax_service.proto + +package opl + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + SyntaxService_Check_FullMethodName = "/ory.keto.opl.v1alpha1.SyntaxService/Check" +) + +// SyntaxServiceClient is the client API for SyntaxService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that checks the syntax of an OPL file. +type SyntaxServiceClient interface { + // Performs a syntax check request. + Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) +} + +type syntaxServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSyntaxServiceClient(cc grpc.ClientConnInterface) SyntaxServiceClient { + return &syntaxServiceClient{cc} +} + +func (c *syntaxServiceClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckResponse) + err := c.cc.Invoke(ctx, SyntaxService_Check_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SyntaxServiceServer is the server API for SyntaxService service. +// All implementations must embed UnimplementedSyntaxServiceServer +// for forward compatibility. +// +// The service that checks the syntax of an OPL file. +type SyntaxServiceServer interface { + // Performs a syntax check request. + Check(context.Context, *CheckRequest) (*CheckResponse, error) + mustEmbedUnimplementedSyntaxServiceServer() +} + +// UnimplementedSyntaxServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSyntaxServiceServer struct{} + +func (UnimplementedSyntaxServiceServer) Check(context.Context, *CheckRequest) (*CheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedSyntaxServiceServer) mustEmbedUnimplementedSyntaxServiceServer() {} +func (UnimplementedSyntaxServiceServer) testEmbeddedByValue() {} + +// UnsafeSyntaxServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SyntaxServiceServer will +// result in compilation errors. +type UnsafeSyntaxServiceServer interface { + mustEmbedUnimplementedSyntaxServiceServer() +} + +func RegisterSyntaxServiceServer(s grpc.ServiceRegistrar, srv SyntaxServiceServer) { + // If the following call panics, it indicates UnimplementedSyntaxServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SyntaxService_ServiceDesc, srv) +} + +func _SyntaxService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SyntaxServiceServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SyntaxService_Check_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SyntaxServiceServer).Check(ctx, req.(*CheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// SyntaxService_ServiceDesc is the grpc.ServiceDesc for SyntaxService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var SyntaxService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.opl.v1alpha1.SyntaxService", + HandlerType: (*SyntaxServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _SyntaxService_Check_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/opl/v1alpha1/syntax_service.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/check_service.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/check_service.pb.go new file mode 100644 index 0000000..a6800c7 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/check_service.pb.go @@ -0,0 +1,591 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/check_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request for a CheckService.Check RPC. +// Checks whether a specific subject is related to an object. +type CheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace to evaluate the check. + // + // Note: If you use the expand-API and the check + // evaluates a RelationTuple specifying a SubjectSet as + // subject or due to a rewrite rule in a namespace config + // this check request may involve other namespaces automatically. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The related object in this check. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between the Object and the Subject. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // The related subject in this check. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + Tuple *RelationTuple `protobuf:"bytes,8,opt,name=tuple,proto3" json:"tuple,omitempty"` + // This field is not implemented yet and has no effect. + // + Latest bool `protobuf:"varint,5,opt,name=latest,proto3" json:"latest,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,6,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + MaxDepth int32 `protobuf:"varint,7,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckRequest) Reset() { + *x = CheckRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckRequest) ProtoMessage() {} + +func (x *CheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckRequest.ProtoReflect.Descriptor instead. +func (*CheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/check_service.proto. +func (x *CheckRequest) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *CheckRequest) GetTuple() *RelationTuple { + if x != nil { + return x.Tuple + } + return nil +} + +func (x *CheckRequest) GetLatest() bool { + if x != nil { + return x.Latest + } + return false +} + +func (x *CheckRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *CheckRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +// The response for a CheckService.Check rpc. +type CheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,2,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponse) Reset() { + *x = CheckResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponse) ProtoMessage() {} + +func (x *CheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponse.ProtoReflect.Descriptor instead. +func (*CheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CheckResponse) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *CheckResponse) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The response for an individual check in the CheckService.BatchCheck rpc. +type CheckResponseWithError struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + // If there was an error checking the tuple, + // this will contain the error message. + // + // If the check was performed successfully, this will be empty. + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResponseWithError) Reset() { + *x = CheckResponseWithError{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResponseWithError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResponseWithError) ProtoMessage() {} + +func (x *CheckResponseWithError) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResponseWithError.ProtoReflect.Descriptor instead. +func (*CheckResponseWithError) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{2} +} + +func (x *CheckResponseWithError) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *CheckResponseWithError) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *CheckResponseWithError) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The request for a CheckService.BatchCheck RPC. +// Checks a batch of relations. +type BatchCheckRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Tuples []*RelationTuple `protobuf:"bytes,1,rep,name=tuples,proto3" json:"tuples,omitempty"` + // This field is not implemented yet and has no effect. + // + Latest bool `protobuf:"varint,2,opt,name=latest,proto3" json:"latest,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + MaxDepth int32 `protobuf:"varint,4,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchCheckRequest) Reset() { + *x = BatchCheckRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchCheckRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchCheckRequest) ProtoMessage() {} + +func (x *BatchCheckRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchCheckRequest.ProtoReflect.Descriptor instead. +func (*BatchCheckRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{3} +} + +func (x *BatchCheckRequest) GetTuples() []*RelationTuple { + if x != nil { + return x.Tuples + } + return nil +} + +func (x *BatchCheckRequest) GetLatest() bool { + if x != nil { + return x.Latest + } + return false +} + +func (x *BatchCheckRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *BatchCheckRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +// The response for a CheckService.BatchCheck rpc. +type BatchCheckResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The results of the batch check. The order of these + // results will match the order of the input. + Results []*CheckResponseWithError `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchCheckResponse) Reset() { + *x = BatchCheckResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchCheckResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchCheckResponse) ProtoMessage() {} + +func (x *BatchCheckResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchCheckResponse.ProtoReflect.Descriptor instead. +func (*BatchCheckResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP(), []int{4} +} + +func (x *BatchCheckResponse) GetResults() []*CheckResponseWithError { + if x != nil { + return x.Results + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_check_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc = "" + + "\n" + + "5ory/keto/relation_tuples/v1alpha2/check_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\xd1\x02\n" + + "\fCheckRequest\x12 \n" + + "\tnamespace\x18\x01 \x01(\tB\x02\x18\x01R\tnamespace\x12\x1a\n" + + "\x06object\x18\x02 \x01(\tB\x02\x18\x01R\x06object\x12\x1e\n" + + "\brelation\x18\x03 \x01(\tB\x02\x18\x01R\brelation\x12H\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectB\x02\x18\x01R\asubject\x12F\n" + + "\x05tuple\x18\b \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x05tuple\x12\x16\n" + + "\x06latest\x18\x05 \x01(\bR\x06latest\x12\x1c\n" + + "\tsnaptoken\x18\x06 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tmax_depth\x18\a \x01(\x05R\bmaxDepth\"G\n" + + "\rCheckResponse\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12\x1c\n" + + "\tsnaptoken\x18\x02 \x01(\tR\tsnaptoken\"f\n" + + "\x16CheckResponseWithError\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\"\xb0\x01\n" + + "\x11BatchCheckRequest\x12H\n" + + "\x06tuples\x18\x01 \x03(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x06tuples\x12\x16\n" + + "\x06latest\x18\x02 \x01(\bR\x06latest\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tmax_depth\x18\x04 \x01(\x05R\bmaxDepth\"i\n" + + "\x12BatchCheckResponse\x12S\n" + + "\aresults\x18\x01 \x03(\v29.ory.keto.relation_tuples.v1alpha2.CheckResponseWithErrorR\aresults2\xf5\x01\n" + + "\fCheckService\x12j\n" + + "\x05Check\x12/.ory.keto.relation_tuples.v1alpha2.CheckRequest\x1a0.ory.keto.relation_tuples.v1alpha2.CheckResponse\x12y\n" + + "\n" + + "BatchCheck\x124.ory.keto.relation_tuples.v1alpha2.BatchCheckRequest\x1a5.ory.keto.relation_tuples.v1alpha2.BatchCheckResponseB\xaa\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x11CheckServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes = []any{ + (*CheckRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.CheckRequest + (*CheckResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.CheckResponse + (*CheckResponseWithError)(nil), // 2: ory.keto.relation_tuples.v1alpha2.CheckResponseWithError + (*BatchCheckRequest)(nil), // 3: ory.keto.relation_tuples.v1alpha2.BatchCheckRequest + (*BatchCheckResponse)(nil), // 4: ory.keto.relation_tuples.v1alpha2.BatchCheckResponse + (*Subject)(nil), // 5: ory.keto.relation_tuples.v1alpha2.Subject + (*RelationTuple)(nil), // 6: ory.keto.relation_tuples.v1alpha2.RelationTuple +} +var file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs = []int32{ + 5, // 0: ory.keto.relation_tuples.v1alpha2.CheckRequest.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 6, // 1: ory.keto.relation_tuples.v1alpha2.CheckRequest.tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 2: ory.keto.relation_tuples.v1alpha2.BatchCheckRequest.tuples:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 2, // 3: ory.keto.relation_tuples.v1alpha2.BatchCheckResponse.results:type_name -> ory.keto.relation_tuples.v1alpha2.CheckResponseWithError + 0, // 4: ory.keto.relation_tuples.v1alpha2.CheckService.Check:input_type -> ory.keto.relation_tuples.v1alpha2.CheckRequest + 3, // 5: ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck:input_type -> ory.keto.relation_tuples.v1alpha2.BatchCheckRequest + 1, // 6: ory.keto.relation_tuples.v1alpha2.CheckService.Check:output_type -> ory.keto.relation_tuples.v1alpha2.CheckResponse + 4, // 7: ory.keto.relation_tuples.v1alpha2.CheckService.BatchCheck:output_type -> ory.keto.relation_tuples.v1alpha2.BatchCheckResponse + 6, // [6:8] is the sub-list for method output_type + 4, // [4:6] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_check_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_check_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_check_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_check_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_check_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_check_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_check_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/check_service.proto b/proto/ory/keto/relation_tuples/v1alpha2/check_service.proto new file mode 100644 index 0000000..bc3dc3d --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/check_service.proto @@ -0,0 +1,185 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +import "ory/keto/relation_tuples/v1alpha2/relation_tuples.proto"; + +// The service that performs authorization checks +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +service CheckService { + // Performs an authorization check. + rpc Check(CheckRequest) returns (CheckResponse); + rpc BatchCheck(BatchCheckRequest) returns (BatchCheckResponse); +} + +// The request for a CheckService.Check RPC. +// Checks whether a specific subject is related to an object. +message CheckRequest { + // The namespace to evaluate the check. + // + // Note: If you use the expand-API and the check + // evaluates a RelationTuple specifying a SubjectSet as + // subject or due to a rewrite rule in a namespace config + // this check request may involve other namespaces automatically. + string namespace = 1 [deprecated = true]; + // The related object in this check. + string object = 2 [deprecated = true]; + // The relation between the Object and the Subject. + string relation = 3 [deprecated = true]; + // The related subject in this check. + Subject subject = 4 [deprecated = true]; + + RelationTuple tuple = 8; + + // This field is not implemented yet and has no effect. + // + bool latest = 5; + // This field is not implemented yet and has no effect. + // + string snaptoken = 6; + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + int32 max_depth = 7; +} + +// The response for a CheckService.Check rpc. +message CheckResponse { + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + bool allowed = 1; + // This field is not implemented yet and has no effect. + // + string snaptoken = 2; +} + +// The response for an individual check in the CheckService.BatchCheck rpc. +message CheckResponseWithError { + // Whether the specified subject (id) + // is related to the requested object. + // + // It is false by default if no ACL matches. + bool allowed = 1; + // If there was an error checking the tuple, + // this will contain the error message. + // + // If the check was performed successfully, this will be empty. + string error = 2; + // This field is not implemented yet and has no effect. + // + string snaptoken = 3; +} + +// The request for a CheckService.BatchCheck RPC. +// Checks a batch of relations. +message BatchCheckRequest { + repeated RelationTuple tuples = 1; + + // This field is not implemented yet and has no effect. + // + bool latest = 2; + // This field is not implemented yet and has no effect. + // + string snaptoken = 3; + // The maximum depth to search for a relation. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + int32 max_depth = 4; +} + +// The response for a CheckService.BatchCheck rpc. +message BatchCheckResponse { + // The results of the batch check. The order of these + // results will match the order of the input. + repeated CheckResponseWithError results = 1; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go new file mode 100644 index 0000000..a1ab9d6 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/check_service_grpc.pb.go @@ -0,0 +1,171 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/check_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + CheckService_Check_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.CheckService/Check" + CheckService_BatchCheck_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.CheckService/BatchCheck" +) + +// CheckServiceClient is the client API for CheckService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that performs authorization checks +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type CheckServiceClient interface { + // Performs an authorization check. + Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) + BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) +} + +type checkServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCheckServiceClient(cc grpc.ClientConnInterface) CheckServiceClient { + return &checkServiceClient{cc} +} + +func (c *checkServiceClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CheckResponse) + err := c.cc.Invoke(ctx, CheckService_Check_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *checkServiceClient) BatchCheck(ctx context.Context, in *BatchCheckRequest, opts ...grpc.CallOption) (*BatchCheckResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BatchCheckResponse) + err := c.cc.Invoke(ctx, CheckService_BatchCheck_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// CheckServiceServer is the server API for CheckService service. +// All implementations must embed UnimplementedCheckServiceServer +// for forward compatibility. +// +// The service that performs authorization checks +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type CheckServiceServer interface { + // Performs an authorization check. + Check(context.Context, *CheckRequest) (*CheckResponse, error) + BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) + mustEmbedUnimplementedCheckServiceServer() +} + +// UnimplementedCheckServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedCheckServiceServer struct{} + +func (UnimplementedCheckServiceServer) Check(context.Context, *CheckRequest) (*CheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Check not implemented") +} +func (UnimplementedCheckServiceServer) BatchCheck(context.Context, *BatchCheckRequest) (*BatchCheckResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchCheck not implemented") +} +func (UnimplementedCheckServiceServer) mustEmbedUnimplementedCheckServiceServer() {} +func (UnimplementedCheckServiceServer) testEmbeddedByValue() {} + +// UnsafeCheckServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to CheckServiceServer will +// result in compilation errors. +type UnsafeCheckServiceServer interface { + mustEmbedUnimplementedCheckServiceServer() +} + +func RegisterCheckServiceServer(s grpc.ServiceRegistrar, srv CheckServiceServer) { + // If the following call panics, it indicates UnimplementedCheckServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&CheckService_ServiceDesc, srv) +} + +func _CheckService_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CheckServiceServer).Check(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CheckService_Check_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CheckServiceServer).Check(ctx, req.(*CheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _CheckService_BatchCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchCheckRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(CheckServiceServer).BatchCheck(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: CheckService_BatchCheck_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(CheckServiceServer).BatchCheck(ctx, req.(*BatchCheckRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// CheckService_ServiceDesc is the grpc.ServiceDesc for CheckService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var CheckService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.CheckService", + HandlerType: (*CheckServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Check", + Handler: _CheckService_Check_Handler, + }, + { + MethodName: "BatchCheck", + Handler: _CheckService_BatchCheck_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/check_service.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go new file mode 100644 index 0000000..7e4a2a5 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/expand_service.pb.go @@ -0,0 +1,385 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/expand_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NodeType int32 + +const ( + NodeType_NODE_TYPE_UNSPECIFIED NodeType = 0 + // This node expands to a union of all children. + NodeType_NODE_TYPE_UNION NodeType = 1 + // Not implemented yet. + NodeType_NODE_TYPE_EXCLUSION NodeType = 2 + // Not implemented yet. + NodeType_NODE_TYPE_INTERSECTION NodeType = 3 + // This node is a leaf and contains no children. + // Its subject is a `SubjectID` unless `max_depth` was reached. + NodeType_NODE_TYPE_LEAF NodeType = 4 +) + +// Enum value maps for NodeType. +var ( + NodeType_name = map[int32]string{ + 0: "NODE_TYPE_UNSPECIFIED", + 1: "NODE_TYPE_UNION", + 2: "NODE_TYPE_EXCLUSION", + 3: "NODE_TYPE_INTERSECTION", + 4: "NODE_TYPE_LEAF", + } + NodeType_value = map[string]int32{ + "NODE_TYPE_UNSPECIFIED": 0, + "NODE_TYPE_UNION": 1, + "NODE_TYPE_EXCLUSION": 2, + "NODE_TYPE_INTERSECTION": 3, + "NODE_TYPE_LEAF": 4, + } +) + +func (x NodeType) Enum() *NodeType { + p := new(NodeType) + *p = x + return p +} + +func (x NodeType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NodeType) Descriptor() protoreflect.EnumDescriptor { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes[0].Descriptor() +} + +func (NodeType) Type() protoreflect.EnumType { + return &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes[0] +} + +func (x NodeType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NodeType.Descriptor instead. +func (NodeType) EnumDescriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{0} +} + +// The request for an ExpandService.Expand RPC. +// Expands the given subject set. +type ExpandRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The subject to expand. + Subject *Subject `protobuf:"bytes,1,opt,name=subject,proto3" json:"subject,omitempty"` + // The maximum depth of tree to build. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + // + // It is important to set this parameter to a meaningful + // value. Ponder how deep you really want to display this. + MaxDepth int32 `protobuf:"varint,2,opt,name=max_depth,json=maxDepth,proto3" json:"max_depth,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpandRequest) Reset() { + *x = ExpandRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpandRequest) ProtoMessage() {} + +func (x *ExpandRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpandRequest.ProtoReflect.Descriptor instead. +func (*ExpandRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ExpandRequest) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *ExpandRequest) GetMaxDepth() int32 { + if x != nil { + return x.MaxDepth + } + return 0 +} + +func (x *ExpandRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +// The response for a ExpandService.Expand RPC. +type ExpandResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The tree the requested subject set expands to. + // The requested subject set is the subject of the root. + // + // This field can be nil in some circumstances. + Tree *SubjectTree `protobuf:"bytes,1,opt,name=tree,proto3" json:"tree,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExpandResponse) Reset() { + *x = ExpandResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExpandResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExpandResponse) ProtoMessage() {} + +func (x *ExpandResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExpandResponse.ProtoReflect.Descriptor instead. +func (*ExpandResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ExpandResponse) GetTree() *SubjectTree { + if x != nil { + return x.Tree + } + return nil +} + +type SubjectTree struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The type of the node. + NodeType NodeType `protobuf:"varint,1,opt,name=node_type,json=nodeType,proto3,enum=ory.keto.relation_tuples.v1alpha2.NodeType" json:"node_type,omitempty"` + // The subject this node represents. + // Deprecated: More information is now available in the tuple field. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/expand_service.proto. + Subject *Subject `protobuf:"bytes,2,opt,name=subject,proto3" json:"subject,omitempty"` + // The relation tuple this node represents. + Tuple *RelationTuple `protobuf:"bytes,4,opt,name=tuple,proto3" json:"tuple,omitempty"` + // The children of this node. + // + // This is never set if `node_type` == `NODE_TYPE_LEAF`. + Children []*SubjectTree `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubjectTree) Reset() { + *x = SubjectTree{} + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubjectTree) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectTree) ProtoMessage() {} + +func (x *SubjectTree) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectTree.ProtoReflect.Descriptor instead. +func (*SubjectTree) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP(), []int{2} +} + +func (x *SubjectTree) GetNodeType() NodeType { + if x != nil { + return x.NodeType + } + return NodeType_NODE_TYPE_UNSPECIFIED +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/expand_service.proto. +func (x *SubjectTree) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +func (x *SubjectTree) GetTuple() *RelationTuple { + if x != nil { + return x.Tuple + } + return nil +} + +func (x *SubjectTree) GetChildren() []*SubjectTree { + if x != nil { + return x.Children + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_expand_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc = "" + + "\n" + + "6ory/keto/relation_tuples/v1alpha2/expand_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\x90\x01\n" + + "\rExpandRequest\x12D\n" + + "\asubject\x18\x01 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\x12\x1b\n" + + "\tmax_depth\x18\x02 \x01(\x05R\bmaxDepth\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\"T\n" + + "\x0eExpandResponse\x12B\n" + + "\x04tree\x18\x01 \x01(\v2..ory.keto.relation_tuples.v1alpha2.SubjectTreeR\x04tree\"\xb5\x02\n" + + "\vSubjectTree\x12H\n" + + "\tnode_type\x18\x01 \x01(\x0e2+.ory.keto.relation_tuples.v1alpha2.NodeTypeR\bnodeType\x12H\n" + + "\asubject\x18\x02 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectB\x02\x18\x01R\asubject\x12F\n" + + "\x05tuple\x18\x04 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x05tuple\x12J\n" + + "\bchildren\x18\x03 \x03(\v2..ory.keto.relation_tuples.v1alpha2.SubjectTreeR\bchildren*\x83\x01\n" + + "\bNodeType\x12\x19\n" + + "\x15NODE_TYPE_UNSPECIFIED\x10\x00\x12\x13\n" + + "\x0fNODE_TYPE_UNION\x10\x01\x12\x17\n" + + "\x13NODE_TYPE_EXCLUSION\x10\x02\x12\x1a\n" + + "\x16NODE_TYPE_INTERSECTION\x10\x03\x12\x12\n" + + "\x0eNODE_TYPE_LEAF\x10\x042~\n" + + "\rExpandService\x12m\n" + + "\x06Expand\x120.ory.keto.relation_tuples.v1alpha2.ExpandRequest\x1a1.ory.keto.relation_tuples.v1alpha2.ExpandResponseB\xab\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x12ExpandServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes = []any{ + (NodeType)(0), // 0: ory.keto.relation_tuples.v1alpha2.NodeType + (*ExpandRequest)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ExpandRequest + (*ExpandResponse)(nil), // 2: ory.keto.relation_tuples.v1alpha2.ExpandResponse + (*SubjectTree)(nil), // 3: ory.keto.relation_tuples.v1alpha2.SubjectTree + (*Subject)(nil), // 4: ory.keto.relation_tuples.v1alpha2.Subject + (*RelationTuple)(nil), // 5: ory.keto.relation_tuples.v1alpha2.RelationTuple +} +var file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs = []int32{ + 4, // 0: ory.keto.relation_tuples.v1alpha2.ExpandRequest.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 3, // 1: ory.keto.relation_tuples.v1alpha2.ExpandResponse.tree:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectTree + 0, // 2: ory.keto.relation_tuples.v1alpha2.SubjectTree.node_type:type_name -> ory.keto.relation_tuples.v1alpha2.NodeType + 4, // 3: ory.keto.relation_tuples.v1alpha2.SubjectTree.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 5, // 4: ory.keto.relation_tuples.v1alpha2.SubjectTree.tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 3, // 5: ory.keto.relation_tuples.v1alpha2.SubjectTree.children:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectTree + 1, // 6: ory.keto.relation_tuples.v1alpha2.ExpandService.Expand:input_type -> ory.keto.relation_tuples.v1alpha2.ExpandRequest + 2, // 7: ory.keto.relation_tuples.v1alpha2.ExpandService.Expand:output_type -> ory.keto.relation_tuples.v1alpha2.ExpandResponse + 7, // [7:8] is the sub-list for method output_type + 6, // [6:7] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_expand_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs, + EnumInfos: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_enumTypes, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_expand_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_expand_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/expand_service.proto b/proto/ory/keto/relation_tuples/v1alpha2/expand_service.proto new file mode 100644 index 0000000..b15cd7f --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/expand_service.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +import "ory/keto/relation_tuples/v1alpha2/relation_tuples.proto"; + +// The service that performs subject set expansion +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +service ExpandService { + // Expands the subject set into a tree of subjects. + rpc Expand(ExpandRequest) returns (ExpandResponse); +} + +// The request for an ExpandService.Expand RPC. +// Expands the given subject set. +message ExpandRequest { + // The subject to expand. + Subject subject = 1; + // The maximum depth of tree to build. + // + // If the value is less than 1 or greater than the global + // max-depth then the global max-depth will be used instead. + // + // It is important to set this parameter to a meaningful + // value. Ponder how deep you really want to display this. + int32 max_depth = 2; + // This field is not implemented yet and has no effect. + // + string snaptoken = 3; +} + +// The response for a ExpandService.Expand RPC. +message ExpandResponse { + // The tree the requested subject set expands to. + // The requested subject set is the subject of the root. + // + // This field can be nil in some circumstances. + SubjectTree tree = 1; +} + +enum NodeType { + NODE_TYPE_UNSPECIFIED = 0; + // This node expands to a union of all children. + NODE_TYPE_UNION = 1; + // Not implemented yet. + NODE_TYPE_EXCLUSION = 2; + // Not implemented yet. + NODE_TYPE_INTERSECTION = 3; + // This node is a leaf and contains no children. + // Its subject is a `SubjectID` unless `max_depth` was reached. + NODE_TYPE_LEAF = 4; +} + +message SubjectTree { + // The type of the node. + NodeType node_type = 1; + + // The subject this node represents. + // Deprecated: More information is now available in the tuple field. + Subject subject = 2 [deprecated = true]; + + // The relation tuple this node represents. + RelationTuple tuple = 4; + + // The children of this node. + // + // This is never set if `node_type` == `NODE_TYPE_LEAF`. + repeated SubjectTree children = 3; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go new file mode 100644 index 0000000..d080f9a --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/expand_service_grpc.pb.go @@ -0,0 +1,133 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/expand_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ExpandService_Expand_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.ExpandService/Expand" +) + +// ExpandServiceClient is the client API for ExpandService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service that performs subject set expansion +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ExpandServiceClient interface { + // Expands the subject set into a tree of subjects. + Expand(ctx context.Context, in *ExpandRequest, opts ...grpc.CallOption) (*ExpandResponse, error) +} + +type expandServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewExpandServiceClient(cc grpc.ClientConnInterface) ExpandServiceClient { + return &expandServiceClient{cc} +} + +func (c *expandServiceClient) Expand(ctx context.Context, in *ExpandRequest, opts ...grpc.CallOption) (*ExpandResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExpandResponse) + err := c.cc.Invoke(ctx, ExpandService_Expand_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ExpandServiceServer is the server API for ExpandService service. +// All implementations must embed UnimplementedExpandServiceServer +// for forward compatibility. +// +// The service that performs subject set expansion +// based on the stored Access Control Lists. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ExpandServiceServer interface { + // Expands the subject set into a tree of subjects. + Expand(context.Context, *ExpandRequest) (*ExpandResponse, error) + mustEmbedUnimplementedExpandServiceServer() +} + +// UnimplementedExpandServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedExpandServiceServer struct{} + +func (UnimplementedExpandServiceServer) Expand(context.Context, *ExpandRequest) (*ExpandResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Expand not implemented") +} +func (UnimplementedExpandServiceServer) mustEmbedUnimplementedExpandServiceServer() {} +func (UnimplementedExpandServiceServer) testEmbeddedByValue() {} + +// UnsafeExpandServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ExpandServiceServer will +// result in compilation errors. +type UnsafeExpandServiceServer interface { + mustEmbedUnimplementedExpandServiceServer() +} + +func RegisterExpandServiceServer(s grpc.ServiceRegistrar, srv ExpandServiceServer) { + // If the following call panics, it indicates UnimplementedExpandServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ExpandService_ServiceDesc, srv) +} + +func _ExpandService_Expand_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExpandRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ExpandServiceServer).Expand(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ExpandService_Expand_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ExpandServiceServer).Expand(ctx, req.(*ExpandRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ExpandService_ServiceDesc is the grpc.ServiceDesc for ExpandService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ExpandService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.ExpandService", + HandlerType: (*ExpandServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Expand", + Handler: _ExpandService_Expand_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/expand_service.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go new file mode 100644 index 0000000..c0bf461 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.pb.go @@ -0,0 +1,216 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for ReadService.ListNamespaces RPC. +type ListNamespacesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNamespacesRequest) Reset() { + *x = ListNamespacesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNamespacesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNamespacesRequest) ProtoMessage() {} + +func (x *ListNamespacesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNamespacesRequest.ProtoReflect.Descriptor instead. +func (*ListNamespacesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{0} +} + +type ListNamespacesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Namespaces []*Namespace `protobuf:"bytes,1,rep,name=namespaces,proto3" json:"namespaces,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNamespacesResponse) Reset() { + *x = ListNamespacesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNamespacesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNamespacesResponse) ProtoMessage() {} + +func (x *ListNamespacesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNamespacesResponse.ProtoReflect.Descriptor instead. +func (*ListNamespacesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListNamespacesResponse) GetNamespaces() []*Namespace { + if x != nil { + return x.Namespaces + } + return nil +} + +type Namespace struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Namespace) Reset() { + *x = Namespace{} + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Namespace) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Namespace) ProtoMessage() {} + +func (x *Namespace) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Namespace.ProtoReflect.Descriptor instead. +func (*Namespace) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP(), []int{2} +} + +func (x *Namespace) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc = "" + + "\n" + + ":ory/keto/relation_tuples/v1alpha2/namespaces_service.proto\x12!ory.keto.relation_tuples.v1alpha2\"\x17\n" + + "\x15ListNamespacesRequest\"f\n" + + "\x16ListNamespacesResponse\x12L\n" + + "\n" + + "namespaces\x18\x01 \x03(\v2,.ory.keto.relation_tuples.v1alpha2.NamespaceR\n" + + "namespaces\"\x1f\n" + + "\tNamespace\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name2\x9b\x01\n" + + "\x11NamespacesService\x12\x85\x01\n" + + "\x0eListNamespaces\x128.ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest\x1a9.ory.keto.relation_tuples.v1alpha2.ListNamespacesResponseB\xaf\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x16NamespacesServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes = []any{ + (*ListNamespacesRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest + (*ListNamespacesResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse + (*Namespace)(nil), // 2: ory.keto.relation_tuples.v1alpha2.Namespace +} +var file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse.namespaces:type_name -> ory.keto.relation_tuples.v1alpha2.Namespace + 0, // 1: ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces:input_type -> ory.keto.relation_tuples.v1alpha2.ListNamespacesRequest + 1, // 2: ory.keto.relation_tuples.v1alpha2.NamespacesService.ListNamespaces:output_type -> ory.keto.relation_tuples.v1alpha2.ListNamespacesResponse + 2, // [2:3] is the sub-list for method output_type + 1, // [1:2] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_namespaces_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.proto b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.proto new file mode 100644 index 0000000..3a66cba --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +// The service to query namespaces. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +service NamespacesService { + // Lists Namespaces + rpc ListNamespaces(ListNamespacesRequest) returns (ListNamespacesResponse); +} + +// Request for ReadService.ListNamespaces RPC. +message ListNamespacesRequest { } + +message ListNamespacesResponse { + repeated Namespace namespaces = 1; +} + +message Namespace { + string name = 1; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go new file mode 100644 index 0000000..54ec3c6 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/namespaces_service_grpc.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/namespaces_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NamespacesService_ListNamespaces_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.NamespacesService/ListNamespaces" +) + +// NamespacesServiceClient is the client API for NamespacesService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service to query namespaces. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type NamespacesServiceClient interface { + // Lists Namespaces + ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) +} + +type namespacesServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNamespacesServiceClient(cc grpc.ClientConnInterface) NamespacesServiceClient { + return &namespacesServiceClient{cc} +} + +func (c *namespacesServiceClient) ListNamespaces(ctx context.Context, in *ListNamespacesRequest, opts ...grpc.CallOption) (*ListNamespacesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListNamespacesResponse) + err := c.cc.Invoke(ctx, NamespacesService_ListNamespaces_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NamespacesServiceServer is the server API for NamespacesService service. +// All implementations must embed UnimplementedNamespacesServiceServer +// for forward compatibility. +// +// The service to query namespaces. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type NamespacesServiceServer interface { + // Lists Namespaces + ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) + mustEmbedUnimplementedNamespacesServiceServer() +} + +// UnimplementedNamespacesServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNamespacesServiceServer struct{} + +func (UnimplementedNamespacesServiceServer) ListNamespaces(context.Context, *ListNamespacesRequest) (*ListNamespacesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListNamespaces not implemented") +} +func (UnimplementedNamespacesServiceServer) mustEmbedUnimplementedNamespacesServiceServer() {} +func (UnimplementedNamespacesServiceServer) testEmbeddedByValue() {} + +// UnsafeNamespacesServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NamespacesServiceServer will +// result in compilation errors. +type UnsafeNamespacesServiceServer interface { + mustEmbedUnimplementedNamespacesServiceServer() +} + +func RegisterNamespacesServiceServer(s grpc.ServiceRegistrar, srv NamespacesServiceServer) { + // If the following call panics, it indicates UnimplementedNamespacesServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NamespacesService_ServiceDesc, srv) +} + +func _NamespacesService_ListNamespaces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListNamespacesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NamespacesServiceServer).ListNamespaces(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NamespacesService_ListNamespaces_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NamespacesServiceServer).ListNamespaces(ctx, req.(*ListNamespacesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NamespacesService_ServiceDesc is the grpc.ServiceDesc for NamespacesService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NamespacesService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.NamespacesService", + HandlerType: (*NamespacesServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListNamespaces", + Handler: _NamespacesService_ListNamespaces_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/namespaces_service.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/read_service.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/read_service.pb.go new file mode 100644 index 0000000..961f7d7 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/read_service.pb.go @@ -0,0 +1,368 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/read_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for ReadService.ListRelationTuples RPC. +// See `ListRelationTuplesRequest_Query` for how to filter the query. +type ListRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // All query constraints are concatenated + // with a logical AND operator. + // + // The RelationTuple list from ListRelationTuplesResponse + // is ordered from the newest RelationTuple to the oldest. + // + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/read_service.proto. + Query *ListRelationTuplesRequest_Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + RelationQuery *RelationQuery `protobuf:"bytes,6,opt,name=relation_query,json=relationQuery,proto3" json:"relation_query,omitempty"` + // This field is not implemented yet and has no effect. + // + ExpandMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=expand_mask,json=expandMask,proto3" json:"expand_mask,omitempty"` + // This field is not implemented yet and has no effect. + // + Snaptoken string `protobuf:"bytes,3,opt,name=snaptoken,proto3" json:"snaptoken,omitempty"` + // Optional. The maximum number of + // RelationTuples to return in the response. + // + // Default: 100 + PageSize int32 `protobuf:"varint,4,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. An opaque pagination token returned from + // a previous call to `ListRelationTuples` that + // indicates where the page should start at. + // + // An empty token denotes the first page. All successive + // pages require the token from the previous page. + PageToken string `protobuf:"bytes,5,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesRequest) Reset() { + *x = ListRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesRequest) ProtoMessage() {} + +func (x *ListRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{0} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/read_service.proto. +func (x *ListRelationTuplesRequest) GetQuery() *ListRelationTuplesRequest_Query { + if x != nil { + return x.Query + } + return nil +} + +func (x *ListRelationTuplesRequest) GetRelationQuery() *RelationQuery { + if x != nil { + return x.RelationQuery + } + return nil +} + +func (x *ListRelationTuplesRequest) GetExpandMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.ExpandMask + } + return nil +} + +func (x *ListRelationTuplesRequest) GetSnaptoken() string { + if x != nil { + return x.Snaptoken + } + return "" +} + +func (x *ListRelationTuplesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListRelationTuplesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +// The response of a ReadService.ListRelationTuples RPC. +type ListRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The relationships matching the list request. + RelationTuples []*RelationTuple `protobuf:"bytes,1,rep,name=relation_tuples,json=relationTuples,proto3" json:"relation_tuples,omitempty"` + // The token required to get the next page. + // If this is the last page, the token will be the empty string. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesResponse) Reset() { + *x = ListRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesResponse) ProtoMessage() {} + +func (x *ListRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListRelationTuplesResponse) GetRelationTuples() []*RelationTuple { + if x != nil { + return x.RelationTuples + } + return nil +} + +func (x *ListRelationTuplesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +// The query for listing relationships. +// Clients can specify any optional field to +// partially filter for specific relationships. +// +// Example use cases (namespace is always required): +// - object only: display a list of all permissions referring to a specific object +// - relation only: get all groups that have members; get all directories that have content +// - object & relation: display all subjects that have a specific permission relation +// - subject & relation: display all groups a subject belongs to; display all objects a subject has access to +// - object & relation & subject: check whether the relation tuple already exists +type ListRelationTuplesRequest_Query struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The namespace to query. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Optional. The object to query for. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // Optional. The relation to query for. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // Optional. The subject to query for. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListRelationTuplesRequest_Query) Reset() { + *x = ListRelationTuplesRequest_Query{} + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListRelationTuplesRequest_Query) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListRelationTuplesRequest_Query) ProtoMessage() {} + +func (x *ListRelationTuplesRequest_Query) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListRelationTuplesRequest_Query.ProtoReflect.Descriptor instead. +func (*ListRelationTuplesRequest_Query) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *ListRelationTuplesRequest_Query) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *ListRelationTuplesRequest_Query) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_read_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc = "" + + "\n" + + "4ory/keto/relation_tuples/v1alpha2/read_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\x1a google/protobuf/field_mask.proto\"\x8b\x04\n" + + "\x19ListRelationTuplesRequest\x12\\\n" + + "\x05query\x18\x01 \x01(\v2B.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.QueryB\x02\x18\x01R\x05query\x12W\n" + + "\x0erelation_query\x18\x06 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationQueryR\rrelationQuery\x12;\n" + + "\vexpand_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskR\n" + + "expandMask\x12\x1c\n" + + "\tsnaptoken\x18\x03 \x01(\tR\tsnaptoken\x12\x1b\n" + + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x05 \x01(\tR\tpageToken\x1a\x9f\x01\n" + + "\x05Query\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\x9f\x01\n" + + "\x1aListRelationTuplesResponse\x12Y\n" + + "\x0frelation_tuples\x18\x01 \x03(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\x0erelationTuples\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken2\xa1\x01\n" + + "\vReadService\x12\x91\x01\n" + + "\x12ListRelationTuples\x12<.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest\x1a=.ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponseB\xa9\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x10ReadServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes = []any{ + (*ListRelationTuplesRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest + (*ListRelationTuplesResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse + (*ListRelationTuplesRequest_Query)(nil), // 2: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query + (*RelationQuery)(nil), // 3: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*fieldmaskpb.FieldMask)(nil), // 4: google.protobuf.FieldMask + (*RelationTuple)(nil), // 5: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*Subject)(nil), // 6: ory.keto.relation_tuples.v1alpha2.Subject +} +var file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.query:type_name -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query + 3, // 1: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.relation_query:type_name -> ory.keto.relation_tuples.v1alpha2.RelationQuery + 4, // 2: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.expand_mask:type_name -> google.protobuf.FieldMask + 5, // 3: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse.relation_tuples:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 4: ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest.Query.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 0, // 5: ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesRequest + 1, // 6: ory.keto.relation_tuples.v1alpha2.ReadService.ListRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.ListRelationTuplesResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_read_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_read_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_read_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_read_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_read_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_read_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_read_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/read_service.proto b/proto/ory/keto/relation_tuples/v1alpha2/read_service.proto new file mode 100644 index 0000000..2f66023 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/read_service.proto @@ -0,0 +1,86 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +import "ory/keto/relation_tuples/v1alpha2/relation_tuples.proto"; +import "google/protobuf/field_mask.proto"; + +// The service to query relationships. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +service ReadService { + // Lists ACL relationships. + rpc ListRelationTuples(ListRelationTuplesRequest) returns (ListRelationTuplesResponse); +} + +// Request for ReadService.ListRelationTuples RPC. +// See `ListRelationTuplesRequest_Query` for how to filter the query. +message ListRelationTuplesRequest { + // The query for listing relationships. + // Clients can specify any optional field to + // partially filter for specific relationships. + // + // Example use cases (namespace is always required): + // - object only: display a list of all permissions referring to a specific object + // - relation only: get all groups that have members; get all directories that have content + // - object & relation: display all subjects that have a specific permission relation + // - subject & relation: display all groups a subject belongs to; display all objects a subject has access to + // - object & relation & subject: check whether the relation tuple already exists + // + message Query { + // Required. The namespace to query. + string namespace = 1; + // Optional. The object to query for. + string object = 2; + // Optional. The relation to query for. + string relation = 3; + // Optional. The subject to query for. + Subject subject = 4; + } + // All query constraints are concatenated + // with a logical AND operator. + // + // The RelationTuple list from ListRelationTuplesResponse + // is ordered from the newest RelationTuple to the oldest. + Query query = 1 [deprecated = true]; + + RelationQuery relation_query = 6; + // This field is not implemented yet and has no effect. + // + google.protobuf.FieldMask expand_mask = 2; + // This field is not implemented yet and has no effect. + // + string snaptoken = 3; + // Optional. The maximum number of + // RelationTuples to return in the response. + // + // Default: 100 + int32 page_size = 4; + // Optional. An opaque pagination token returned from + // a previous call to `ListRelationTuples` that + // indicates where the page should start at. + // + // An empty token denotes the first page. All successive + // pages require the token from the previous page. + string page_token = 5; +} + +// The response of a ReadService.ListRelationTuples RPC. +message ListRelationTuplesResponse { + // The relationships matching the list request. + repeated RelationTuple relation_tuples = 1; + // The token required to get the next page. + // If this is the last page, the token will be the empty string. + string next_page_token = 2; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go new file mode 100644 index 0000000..aa0e014 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/read_service_grpc.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/read_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ReadService_ListRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.ReadService/ListRelationTuples" +) + +// ReadServiceClient is the client API for ReadService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service to query relationships. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ReadServiceClient interface { + // Lists ACL relationships. + ListRelationTuples(ctx context.Context, in *ListRelationTuplesRequest, opts ...grpc.CallOption) (*ListRelationTuplesResponse, error) +} + +type readServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewReadServiceClient(cc grpc.ClientConnInterface) ReadServiceClient { + return &readServiceClient{cc} +} + +func (c *readServiceClient) ListRelationTuples(ctx context.Context, in *ListRelationTuplesRequest, opts ...grpc.CallOption) (*ListRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListRelationTuplesResponse) + err := c.cc.Invoke(ctx, ReadService_ListRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ReadServiceServer is the server API for ReadService service. +// All implementations must embed UnimplementedReadServiceServer +// for forward compatibility. +// +// The service to query relationships. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis). +type ReadServiceServer interface { + // Lists ACL relationships. + ListRelationTuples(context.Context, *ListRelationTuplesRequest) (*ListRelationTuplesResponse, error) + mustEmbedUnimplementedReadServiceServer() +} + +// UnimplementedReadServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedReadServiceServer struct{} + +func (UnimplementedReadServiceServer) ListRelationTuples(context.Context, *ListRelationTuplesRequest) (*ListRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListRelationTuples not implemented") +} +func (UnimplementedReadServiceServer) mustEmbedUnimplementedReadServiceServer() {} +func (UnimplementedReadServiceServer) testEmbeddedByValue() {} + +// UnsafeReadServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ReadServiceServer will +// result in compilation errors. +type UnsafeReadServiceServer interface { + mustEmbedUnimplementedReadServiceServer() +} + +func RegisterReadServiceServer(s grpc.ServiceRegistrar, srv ReadServiceServer) { + // If the following call panics, it indicates UnimplementedReadServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ReadService_ServiceDesc, srv) +} + +func _ReadService_ListRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReadServiceServer).ListRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ReadService_ListRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReadServiceServer).ListRelationTuples(ctx, req.(*ListRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ReadService_ServiceDesc is the grpc.ServiceDesc for ReadService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ReadService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.ReadService", + HandlerType: (*ReadServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListRelationTuples", + Handler: _ReadService_ListRelationTuples_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/read_service.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go new file mode 100644 index 0000000..8fc98fa --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.pb.go @@ -0,0 +1,429 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/relation_tuples.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RelationTuple defines a relation between an Object and a Subject. +type RelationTuple struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace this relation tuple lives in. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The object related by this tuple. + // It is an object in the namespace of the tuple. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between an Object and a Subject. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationTuple) Reset() { + *x = RelationTuple{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationTuple) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationTuple) ProtoMessage() {} + +func (x *RelationTuple) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationTuple.ProtoReflect.Descriptor instead. +func (*RelationTuple) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{0} +} + +func (x *RelationTuple) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *RelationTuple) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *RelationTuple) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *RelationTuple) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +// The query for listing relationships. +// Clients can specify any optional field to +// partially filter for specific relationships. +// +// Example use cases (namespace is always required): +// - object only: display a list of all permissions referring to a specific object +// - relation only: get all groups that have members; get all directories that have content +// - object & relation: display all subjects that have a specific permission relation +// - subject & relation: display all groups a subject belongs to; display all objects a subject has access to +// - object & relation & subject: check whether the relation tuple already exists +type RelationQuery struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace this relation tuple lives in. + Namespace *string `protobuf:"bytes,1,opt,name=namespace,proto3,oneof" json:"namespace,omitempty"` + // The object related by this tuple. + // It is an object in the namespace of the tuple. + Object *string `protobuf:"bytes,2,opt,name=object,proto3,oneof" json:"object,omitempty"` + // The relation between an Object and a Subject. + Relation *string `protobuf:"bytes,3,opt,name=relation,proto3,oneof" json:"relation,omitempty"` + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3,oneof" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationQuery) Reset() { + *x = RelationQuery{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationQuery) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationQuery) ProtoMessage() {} + +func (x *RelationQuery) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationQuery.ProtoReflect.Descriptor instead. +func (*RelationQuery) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{1} +} + +func (x *RelationQuery) GetNamespace() string { + if x != nil && x.Namespace != nil { + return *x.Namespace + } + return "" +} + +func (x *RelationQuery) GetObject() string { + if x != nil && x.Object != nil { + return *x.Object + } + return "" +} + +func (x *RelationQuery) GetRelation() string { + if x != nil && x.Relation != nil { + return *x.Relation + } + return "" +} + +func (x *RelationQuery) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +// Subject is either a concrete subject id or +// a `SubjectSet` expanding to more Subjects. +type Subject struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The reference of this abstract subject. + // + // Types that are valid to be assigned to Ref: + // + // *Subject_Id + // *Subject_Set + Ref isSubject_Ref `protobuf_oneof:"ref"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Subject) Reset() { + *x = Subject{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Subject) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Subject) ProtoMessage() {} + +func (x *Subject) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Subject.ProtoReflect.Descriptor instead. +func (*Subject) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{2} +} + +func (x *Subject) GetRef() isSubject_Ref { + if x != nil { + return x.Ref + } + return nil +} + +func (x *Subject) GetId() string { + if x != nil { + if x, ok := x.Ref.(*Subject_Id); ok { + return x.Id + } + } + return "" +} + +func (x *Subject) GetSet() *SubjectSet { + if x != nil { + if x, ok := x.Ref.(*Subject_Set); ok { + return x.Set + } + } + return nil +} + +type isSubject_Ref interface { + isSubject_Ref() +} + +type Subject_Id struct { + // A concrete id of the subject. + Id string `protobuf:"bytes,1,opt,name=id,proto3,oneof"` +} + +type Subject_Set struct { + // A subject set that expands to more Subjects. + // More information are available under [concepts](../concepts/15_subjects.mdx). + Set *SubjectSet `protobuf:"bytes,2,opt,name=set,proto3,oneof"` +} + +func (*Subject_Id) isSubject_Ref() {} + +func (*Subject_Set) isSubject_Ref() {} + +// SubjectSet refers to all subjects who have +// the same `relation` on an `object`. +type SubjectSet struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The namespace of the object and relation + // referenced in this subject set. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // The object related by this subject set. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // The relation between the object and the subjects. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubjectSet) Reset() { + *x = SubjectSet{} + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubjectSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubjectSet) ProtoMessage() {} + +func (x *SubjectSet) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubjectSet.ProtoReflect.Descriptor instead. +func (*SubjectSet) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP(), []int{3} +} + +func (x *SubjectSet) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *SubjectSet) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *SubjectSet) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc = "" + + "\n" + + "7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\x12!ory.keto.relation_tuples.v1alpha2\"\xa7\x01\n" + + "\rRelationTuple\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\xed\x01\n" + + "\rRelationQuery\x12!\n" + + "\tnamespace\x18\x01 \x01(\tH\x00R\tnamespace\x88\x01\x01\x12\x1b\n" + + "\x06object\x18\x02 \x01(\tH\x01R\x06object\x88\x01\x01\x12\x1f\n" + + "\brelation\x18\x03 \x01(\tH\x02R\brelation\x88\x01\x01\x12I\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectH\x03R\asubject\x88\x01\x01B\f\n" + + "\n" + + "_namespaceB\t\n" + + "\a_objectB\v\n" + + "\t_relationB\n" + + "\n" + + "\b_subject\"e\n" + + "\aSubject\x12\x10\n" + + "\x02id\x18\x01 \x01(\tH\x00R\x02id\x12A\n" + + "\x03set\x18\x02 \x01(\v2-.ory.keto.relation_tuples.v1alpha2.SubjectSetH\x00R\x03setB\x05\n" + + "\x03ref\"^\n" + + "\n" + + "SubjectSet\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelationB\xac\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x13RelationTuplesProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes = []any{ + (*RelationTuple)(nil), // 0: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*RelationQuery)(nil), // 1: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*Subject)(nil), // 2: ory.keto.relation_tuples.v1alpha2.Subject + (*SubjectSet)(nil), // 3: ory.keto.relation_tuples.v1alpha2.SubjectSet +} +var file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.RelationTuple.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 2, // 1: ory.keto.relation_tuples.v1alpha2.RelationQuery.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 3, // 2: ory.keto.relation_tuples.v1alpha2.Subject.set:type_name -> ory.keto.relation_tuples.v1alpha2.SubjectSet + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[1].OneofWrappers = []any{} + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes[2].OneofWrappers = []any{ + (*Subject_Id)(nil), + (*Subject_Set)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.proto b/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.proto new file mode 100644 index 0000000..1608f44 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/relation_tuples.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +// RelationTuple defines a relation between an Object and a Subject. +message RelationTuple { + // The namespace this relation tuple lives in. + string namespace = 1; + // The object related by this tuple. + // It is an object in the namespace of the tuple. + string object = 2; + // The relation between an Object and a Subject. + string relation = 3; + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + Subject subject = 4; +} + +// The query for listing relationships. +// Clients can specify any optional field to +// partially filter for specific relationships. +// +// Example use cases (namespace is always required): +// - object only: display a list of all permissions referring to a specific object +// - relation only: get all groups that have members; get all directories that have content +// - object & relation: display all subjects that have a specific permission relation +// - subject & relation: display all groups a subject belongs to; display all objects a subject has access to +// - object & relation & subject: check whether the relation tuple already exists +message RelationQuery { + // The namespace this relation tuple lives in. + optional string namespace = 1; + // The object related by this tuple. + // It is an object in the namespace of the tuple. + optional string object = 2; + // The relation between an Object and a Subject. + optional string relation = 3; + // The subject related by this tuple. + // A Subject either represents a concrete subject id or + // a `SubjectSet` that expands to more Subjects. + optional Subject subject = 4; +} + +// Subject is either a concrete subject id or +// a `SubjectSet` expanding to more Subjects. +message Subject { + // The reference of this abstract subject. + oneof ref { + // A concrete id of the subject. + string id = 1; + // A subject set that expands to more Subjects. + // More information are available under [concepts](../concepts/15_subjects.mdx). + SubjectSet set = 2; + } +} + +// SubjectSet refers to all subjects who have +// the same `relation` on an `object`. +message SubjectSet { + // The namespace of the object and relation + // referenced in this subject set. + string namespace = 1; + // The object related by this subject set. + string object = 2; + // The relation between the object and the subjects. + string relation = 3; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/utils.go b/proto/ory/keto/relation_tuples/v1alpha2/utils.go new file mode 100644 index 0000000..32df8ff --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/utils.go @@ -0,0 +1,35 @@ +package relationtuples + +// RelationTupleToDeltas is a helper that converts a slice of RelationTuple to a slice of RelationTupleDelta +// with the specified RelationTupleDelta_Action. This allows you to conveniently assemble a request for the +// WriteServiceClient.TransactRelationTuples operation. +// +// Example: +// +// c.TransactRelationTuples(context.Background(), &rts.TransactRelationTuplesRequest{ +// RelationTupleDeltas: append(rts.RelationTupleToDeltas(insertTuples, rts.RelationTupleDelta_INSERT), rts.RelationTupleToDeltas(deleteTuples, rts.RelationTupleDelta_DELETE)...), +// }) +func RelationTupleToDeltas(rs []*RelationTuple, action RelationTupleDelta_Action) []*RelationTupleDelta { + deltas := make([]*RelationTupleDelta, len(rs)) + for i := range rs { + deltas[i] = &RelationTupleDelta{ + RelationTuple: rs[i], + Action: action, + } + } + return deltas +} + +// NewSubjectSet returns a Subject with a SubjectSet ref. +func NewSubjectSet(namespace, object, relation string) *Subject { + return &Subject{Ref: &Subject_Set{Set: &SubjectSet{ + Namespace: namespace, + Object: object, + Relation: relation, + }}} +} + +// NewSubjectID returns a Subject with a subject ID ref. +func NewSubjectID(id string) *Subject { + return &Subject{Ref: &Subject_Id{Id: id}} +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/utils_test.go b/proto/ory/keto/relation_tuples/v1alpha2/utils_test.go new file mode 100644 index 0000000..ec92ec5 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/utils_test.go @@ -0,0 +1,64 @@ +package relationtuples + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRelationTupleToDeltas(t *testing.T) { + for _, tc := range []struct { + desc string + rts []*RelationTuple + action RelationTupleDelta_Action + expected []*RelationTupleDelta + }{ + { + desc: "empty list", + rts: nil, + action: RelationTupleDelta_ACTION_INSERT, + expected: []*RelationTupleDelta{}, + }, + { + desc: "some tuples", + rts: []*RelationTuple{ + { + Namespace: "n", + Object: "o1", + Relation: "r", + Subject: NewSubjectID("s"), + }, { + Namespace: "n", + Object: "o2", + Relation: "r", + Subject: NewSubjectSet("sn", "so", "sr"), + }, + }, + action: RelationTupleDelta_ACTION_DELETE, + expected: []*RelationTupleDelta{ + { + RelationTuple: &RelationTuple{ + Namespace: "n", + Object: "o1", + Relation: "r", + Subject: NewSubjectID("s"), + }, + Action: RelationTupleDelta_ACTION_DELETE, + }, + { + RelationTuple: &RelationTuple{ + Namespace: "n", + Object: "o2", + Relation: "r", + Subject: NewSubjectSet("sn", "so", "sr"), + }, + Action: RelationTupleDelta_ACTION_DELETE, + }, + }, + }, + } { + t.Run("case="+tc.desc, func(t *testing.T) { + assert.Equal(t, tc.expected, RelationTupleToDeltas(tc.rts, tc.action)) + }) + } +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/version.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/version.pb.go new file mode 100644 index 0000000..e485de1 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/version.pb.go @@ -0,0 +1,169 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/version.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Request for the VersionService.GetVersion RPC. +type GetVersionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionRequest) Reset() { + *x = GetVersionRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionRequest) ProtoMessage() {} + +func (x *GetVersionRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVersionRequest.ProtoReflect.Descriptor instead. +func (*GetVersionRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP(), []int{0} +} + +// Response of the VersionService.GetVersion RPC. +type GetVersionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The version string of the Ory Keto instance. + Version string `protobuf:"bytes,1,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVersionResponse) Reset() { + *x = GetVersionResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVersionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVersionResponse) ProtoMessage() {} + +func (x *GetVersionResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVersionResponse.ProtoReflect.Descriptor instead. +func (*GetVersionResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP(), []int{1} +} + +func (x *GetVersionResponse) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +var File_ory_keto_relation_tuples_v1alpha2_version_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc = "" + + "\n" + + "/ory/keto/relation_tuples/v1alpha2/version.proto\x12!ory.keto.relation_tuples.v1alpha2\"\x13\n" + + "\x11GetVersionRequest\".\n" + + "\x12GetVersionResponse\x12\x18\n" + + "\aversion\x18\x01 \x01(\tR\aversion2\x8b\x01\n" + + "\x0eVersionService\x12y\n" + + "\n" + + "GetVersion\x124.ory.keto.relation_tuples.v1alpha2.GetVersionRequest\x1a5.ory.keto.relation_tuples.v1alpha2.GetVersionResponseB\xa5\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\fVersionProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes = []any{ + (*GetVersionRequest)(nil), // 0: ory.keto.relation_tuples.v1alpha2.GetVersionRequest + (*GetVersionResponse)(nil), // 1: ory.keto.relation_tuples.v1alpha2.GetVersionResponse +} +var file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs = []int32{ + 0, // 0: ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion:input_type -> ory.keto.relation_tuples.v1alpha2.GetVersionRequest + 1, // 1: ory.keto.relation_tuples.v1alpha2.VersionService.GetVersion:output_type -> ory.keto.relation_tuples.v1alpha2.GetVersionResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_version_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_version_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_version_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_version_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_version_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_version_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_version_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_version_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/version.proto b/proto/ory/keto/relation_tuples/v1alpha2/version.proto new file mode 100644 index 0000000..60243b7 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/version.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +// The service returning the specific Ory Keto instance version. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). +service VersionService { + // Returns the version of the Ory Keto instance. + rpc GetVersion(GetVersionRequest) returns (GetVersionResponse); +} + +// Request for the VersionService.GetVersion RPC. +message GetVersionRequest {} + +// Response of the VersionService.GetVersion RPC. +message GetVersionResponse { + // The version string of the Ory Keto instance. + string version = 1; +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go new file mode 100644 index 0000000..4f5a9b3 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/version_grpc.pb.go @@ -0,0 +1,131 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/version.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + VersionService_GetVersion_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.VersionService/GetVersion" +) + +// VersionServiceClient is the client API for VersionService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The service returning the specific Ory Keto instance version. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type VersionServiceClient interface { + // Returns the version of the Ory Keto instance. + GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) +} + +type versionServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewVersionServiceClient(cc grpc.ClientConnInterface) VersionServiceClient { + return &versionServiceClient{cc} +} + +func (c *versionServiceClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetVersionResponse) + err := c.cc.Invoke(ctx, VersionService_GetVersion_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VersionServiceServer is the server API for VersionService service. +// All implementations must embed UnimplementedVersionServiceServer +// for forward compatibility. +// +// The service returning the specific Ory Keto instance version. +// +// This service is part of the [read-APIs](../concepts/25_api-overview.mdx#read-apis) and [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type VersionServiceServer interface { + // Returns the version of the Ory Keto instance. + GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) + mustEmbedUnimplementedVersionServiceServer() +} + +// UnimplementedVersionServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedVersionServiceServer struct{} + +func (UnimplementedVersionServiceServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetVersion not implemented") +} +func (UnimplementedVersionServiceServer) mustEmbedUnimplementedVersionServiceServer() {} +func (UnimplementedVersionServiceServer) testEmbeddedByValue() {} + +// UnsafeVersionServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VersionServiceServer will +// result in compilation errors. +type UnsafeVersionServiceServer interface { + mustEmbedUnimplementedVersionServiceServer() +} + +func RegisterVersionServiceServer(s grpc.ServiceRegistrar, srv VersionServiceServer) { + // If the following call panics, it indicates UnimplementedVersionServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&VersionService_ServiceDesc, srv) +} + +func _VersionService_GetVersion_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetVersionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VersionServiceServer).GetVersion(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VersionService_GetVersion_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VersionServiceServer).GetVersion(ctx, req.(*GetVersionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// VersionService_ServiceDesc is the grpc.ServiceDesc for VersionService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var VersionService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.VersionService", + HandlerType: (*VersionServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetVersion", + Handler: _VersionService_GetVersion_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/version.proto", +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/write_service.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/write_service.pb.go new file mode 100644 index 0000000..e9b6cf5 --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/write_service.pb.go @@ -0,0 +1,499 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/write_service.proto + +package relationtuples + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type RelationTupleDelta_Action int32 + +const ( + // Unspecified. + // The `TransactRelationTuples` RPC ignores this + // RelationTupleDelta if an action was unspecified. + RelationTupleDelta_ACTION_UNSPECIFIED RelationTupleDelta_Action = 0 + // Insertion of a new RelationTuple. + // It is ignored if already existing. + RelationTupleDelta_ACTION_INSERT RelationTupleDelta_Action = 1 + // Deletion of the RelationTuple. + // It is ignored if it does not exist. + RelationTupleDelta_ACTION_DELETE RelationTupleDelta_Action = 2 +) + +// Enum value maps for RelationTupleDelta_Action. +var ( + RelationTupleDelta_Action_name = map[int32]string{ + 0: "ACTION_UNSPECIFIED", + 1: "ACTION_INSERT", + 2: "ACTION_DELETE", + } + RelationTupleDelta_Action_value = map[string]int32{ + "ACTION_UNSPECIFIED": 0, + "ACTION_INSERT": 1, + "ACTION_DELETE": 2, + } +) + +func (x RelationTupleDelta_Action) Enum() *RelationTupleDelta_Action { + p := new(RelationTupleDelta_Action) + *p = x + return p +} + +func (x RelationTupleDelta_Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (RelationTupleDelta_Action) Descriptor() protoreflect.EnumDescriptor { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes[0].Descriptor() +} + +func (RelationTupleDelta_Action) Type() protoreflect.EnumType { + return &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes[0] +} + +func (x RelationTupleDelta_Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use RelationTupleDelta_Action.Descriptor instead. +func (RelationTupleDelta_Action) EnumDescriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{1, 0} +} + +// The request of a WriteService.TransactRelationTuples RPC. +type TransactRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The write delta for the relationships operated in one single transaction. + // Either all actions succeed or no change takes effect on error. + RelationTupleDeltas []*RelationTupleDelta `protobuf:"bytes,1,rep,name=relation_tuple_deltas,json=relationTupleDeltas,proto3" json:"relation_tuple_deltas,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactRelationTuplesRequest) Reset() { + *x = TransactRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactRelationTuplesRequest) ProtoMessage() {} + +func (x *TransactRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransactRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*TransactRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{0} +} + +func (x *TransactRelationTuplesRequest) GetRelationTupleDeltas() []*RelationTupleDelta { + if x != nil { + return x.RelationTupleDeltas + } + return nil +} + +// Write-delta for a TransactRelationTuplesRequest. +type RelationTupleDelta struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The action to do on the RelationTuple. + Action RelationTupleDelta_Action `protobuf:"varint,1,opt,name=action,proto3,enum=ory.keto.relation_tuples.v1alpha2.RelationTupleDelta_Action" json:"action,omitempty"` + // The target RelationTuple. + RelationTuple *RelationTuple `protobuf:"bytes,2,opt,name=relation_tuple,json=relationTuple,proto3" json:"relation_tuple,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelationTupleDelta) Reset() { + *x = RelationTupleDelta{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelationTupleDelta) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelationTupleDelta) ProtoMessage() {} + +func (x *RelationTupleDelta) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelationTupleDelta.ProtoReflect.Descriptor instead. +func (*RelationTupleDelta) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{1} +} + +func (x *RelationTupleDelta) GetAction() RelationTupleDelta_Action { + if x != nil { + return x.Action + } + return RelationTupleDelta_ACTION_UNSPECIFIED +} + +func (x *RelationTupleDelta) GetRelationTuple() *RelationTuple { + if x != nil { + return x.RelationTuple + } + return nil +} + +// The response of a WriteService.TransactRelationTuples rpc. +type TransactRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This field is not implemented yet and has no effect. + // + Snaptokens []string `protobuf:"bytes,1,rep,name=snaptokens,proto3" json:"snaptokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransactRelationTuplesResponse) Reset() { + *x = TransactRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransactRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransactRelationTuplesResponse) ProtoMessage() {} + +func (x *TransactRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransactRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*TransactRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{2} +} + +func (x *TransactRelationTuplesResponse) GetSnaptokens() []string { + if x != nil { + return x.Snaptokens + } + return nil +} + +type DeleteRelationTuplesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/write_service.proto. + Query *DeleteRelationTuplesRequest_Query `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + RelationQuery *RelationQuery `protobuf:"bytes,2,opt,name=relation_query,json=relationQuery,proto3" json:"relation_query,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesRequest) Reset() { + *x = DeleteRelationTuplesRequest{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesRequest) ProtoMessage() {} + +func (x *DeleteRelationTuplesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesRequest.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesRequest) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{3} +} + +// Deprecated: Marked as deprecated in ory/keto/relation_tuples/v1alpha2/write_service.proto. +func (x *DeleteRelationTuplesRequest) GetQuery() *DeleteRelationTuplesRequest_Query { + if x != nil { + return x.Query + } + return nil +} + +func (x *DeleteRelationTuplesRequest) GetRelationQuery() *RelationQuery { + if x != nil { + return x.RelationQuery + } + return nil +} + +type DeleteRelationTuplesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesResponse) Reset() { + *x = DeleteRelationTuplesResponse{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesResponse) ProtoMessage() {} + +func (x *DeleteRelationTuplesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesResponse.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesResponse) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{4} +} + +// The query for deleting relationships +type DeleteRelationTuplesRequest_Query struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The namespace to query. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Optional. The object to query for. + Object string `protobuf:"bytes,2,opt,name=object,proto3" json:"object,omitempty"` + // Optional. The relation to query for. + Relation string `protobuf:"bytes,3,opt,name=relation,proto3" json:"relation,omitempty"` + // Optional. The subject to query for. + Subject *Subject `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteRelationTuplesRequest_Query) Reset() { + *x = DeleteRelationTuplesRequest_Query{} + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteRelationTuplesRequest_Query) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteRelationTuplesRequest_Query) ProtoMessage() {} + +func (x *DeleteRelationTuplesRequest_Query) ProtoReflect() protoreflect.Message { + mi := &file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteRelationTuplesRequest_Query.ProtoReflect.Descriptor instead. +func (*DeleteRelationTuplesRequest_Query) Descriptor() ([]byte, []int) { + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *DeleteRelationTuplesRequest_Query) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetObject() string { + if x != nil { + return x.Object + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetRelation() string { + if x != nil { + return x.Relation + } + return "" +} + +func (x *DeleteRelationTuplesRequest_Query) GetSubject() *Subject { + if x != nil { + return x.Subject + } + return nil +} + +var File_ory_keto_relation_tuples_v1alpha2_write_service_proto protoreflect.FileDescriptor + +const file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc = "" + + "\n" + + "5ory/keto/relation_tuples/v1alpha2/write_service.proto\x12!ory.keto.relation_tuples.v1alpha2\x1a7ory/keto/relation_tuples/v1alpha2/relation_tuples.proto\"\x8a\x01\n" + + "\x1dTransactRelationTuplesRequest\x12i\n" + + "\x15relation_tuple_deltas\x18\x01 \x03(\v25.ory.keto.relation_tuples.v1alpha2.RelationTupleDeltaR\x13relationTupleDeltas\"\x8b\x02\n" + + "\x12RelationTupleDelta\x12T\n" + + "\x06action\x18\x01 \x01(\x0e2<.ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.ActionR\x06action\x12W\n" + + "\x0erelation_tuple\x18\x02 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationTupleR\rrelationTuple\"F\n" + + "\x06Action\x12\x16\n" + + "\x12ACTION_UNSPECIFIED\x10\x00\x12\x11\n" + + "\rACTION_INSERT\x10\x01\x12\x11\n" + + "\rACTION_DELETE\x10\x02\"@\n" + + "\x1eTransactRelationTuplesResponse\x12\x1e\n" + + "\n" + + "snaptokens\x18\x01 \x03(\tR\n" + + "snaptokens\"\xf8\x02\n" + + "\x1bDeleteRelationTuplesRequest\x12^\n" + + "\x05query\x18\x01 \x01(\v2D.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.QueryB\x02\x18\x01R\x05query\x12W\n" + + "\x0erelation_query\x18\x02 \x01(\v20.ory.keto.relation_tuples.v1alpha2.RelationQueryR\rrelationQuery\x1a\x9f\x01\n" + + "\x05Query\x12\x1c\n" + + "\tnamespace\x18\x01 \x01(\tR\tnamespace\x12\x16\n" + + "\x06object\x18\x02 \x01(\tR\x06object\x12\x1a\n" + + "\brelation\x18\x03 \x01(\tR\brelation\x12D\n" + + "\asubject\x18\x04 \x01(\v2*.ory.keto.relation_tuples.v1alpha2.SubjectR\asubject\"\x1e\n" + + "\x1cDeleteRelationTuplesResponse2\xc8\x02\n" + + "\fWriteService\x12\x9d\x01\n" + + "\x16TransactRelationTuples\x12@.ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest\x1aA.ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse\x12\x97\x01\n" + + "\x14DeleteRelationTuples\x12>.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest\x1a?.ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponseB\xaa\x02\n" + + "%com.ory.keto.relation_tuples.v1alpha2B\x11WriteServiceProtoP\x01ZKgithub.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2;relationtuples\xa2\x02\x03OKR\xaa\x02 Ory.Keto.RelationTuples.V1alpha2\xca\x02 Ory\\Keto\\RelationTuples\\V1alpha2\xe2\x02,Ory\\Keto\\RelationTuples\\V1alpha2\\GPBMetadata\xea\x02#Ory::Keto::RelationTuples::V1alpha2b\x06proto3" + +var ( + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescOnce sync.Once + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData []byte +) + +func file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescGZIP() []byte { + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescOnce.Do(func() { + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc))) + }) + return file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDescData +} + +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes = []any{ + (RelationTupleDelta_Action)(0), // 0: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + (*TransactRelationTuplesRequest)(nil), // 1: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest + (*RelationTupleDelta)(nil), // 2: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta + (*TransactRelationTuplesResponse)(nil), // 3: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse + (*DeleteRelationTuplesRequest)(nil), // 4: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest + (*DeleteRelationTuplesResponse)(nil), // 5: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse + (*DeleteRelationTuplesRequest_Query)(nil), // 6: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query + (*RelationTuple)(nil), // 7: ory.keto.relation_tuples.v1alpha2.RelationTuple + (*RelationQuery)(nil), // 8: ory.keto.relation_tuples.v1alpha2.RelationQuery + (*Subject)(nil), // 9: ory.keto.relation_tuples.v1alpha2.Subject +} +var file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs = []int32{ + 2, // 0: ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest.relation_tuple_deltas:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTupleDelta + 0, // 1: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.action:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.Action + 7, // 2: ory.keto.relation_tuples.v1alpha2.RelationTupleDelta.relation_tuple:type_name -> ory.keto.relation_tuples.v1alpha2.RelationTuple + 6, // 3: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.query:type_name -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query + 8, // 4: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.relation_query:type_name -> ory.keto.relation_tuples.v1alpha2.RelationQuery + 9, // 5: ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest.Query.subject:type_name -> ory.keto.relation_tuples.v1alpha2.Subject + 1, // 6: ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesRequest + 4, // 7: ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples:input_type -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesRequest + 3, // 8: ory.keto.relation_tuples.v1alpha2.WriteService.TransactRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.TransactRelationTuplesResponse + 5, // 9: ory.keto.relation_tuples.v1alpha2.WriteService.DeleteRelationTuples:output_type -> ory.keto.relation_tuples.v1alpha2.DeleteRelationTuplesResponse + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_ory_keto_relation_tuples_v1alpha2_write_service_proto_init() } +func file_ory_keto_relation_tuples_v1alpha2_write_service_proto_init() { + if File_ory_keto_relation_tuples_v1alpha2_write_service_proto != nil { + return + } + file_ory_keto_relation_tuples_v1alpha2_relation_tuples_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc), len(file_ory_keto_relation_tuples_v1alpha2_write_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes, + DependencyIndexes: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs, + EnumInfos: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_enumTypes, + MessageInfos: file_ory_keto_relation_tuples_v1alpha2_write_service_proto_msgTypes, + }.Build() + File_ory_keto_relation_tuples_v1alpha2_write_service_proto = out.File + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_goTypes = nil + file_ory_keto_relation_tuples_v1alpha2_write_service_proto_depIdxs = nil +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/write_service.proto b/proto/ory/keto/relation_tuples/v1alpha2/write_service.proto new file mode 100644 index 0000000..a16b3ba --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/write_service.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package ory.keto.relation_tuples.v1alpha2; + +import "ory/keto/relation_tuples/v1alpha2/relation_tuples.proto"; + +// The write service to create and delete Access Control Lists. +// +// This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). +service WriteService { + // Writes one or more relationships in a single transaction. + rpc TransactRelationTuples(TransactRelationTuplesRequest) returns (TransactRelationTuplesResponse); + // Deletes relationships based on relation query + rpc DeleteRelationTuples(DeleteRelationTuplesRequest) returns (DeleteRelationTuplesResponse); +} + +// The request of a WriteService.TransactRelationTuples RPC. +message TransactRelationTuplesRequest { + // The write delta for the relationships operated in one single transaction. + // Either all actions succeed or no change takes effect on error. + repeated RelationTupleDelta relation_tuple_deltas = 1; +} + +// Write-delta for a TransactRelationTuplesRequest. +message RelationTupleDelta { + enum Action { + // Unspecified. + // The `TransactRelationTuples` RPC ignores this + // RelationTupleDelta if an action was unspecified. + ACTION_UNSPECIFIED = 0; + + // Insertion of a new RelationTuple. + ACTION_INSERT = 1; + + // Deletion of the RelationTuple. + // It is ignored if it does not exist. + ACTION_DELETE = 2; + } + // The action to do on the RelationTuple. + Action action = 1; + // The target RelationTuple. + RelationTuple relation_tuple = 2; +} + +// The response of a WriteService.TransactRelationTuples rpc. +message TransactRelationTuplesResponse { + // This field is not implemented yet and has no effect. + // + repeated string snaptokens = 1; +} + +message DeleteRelationTuplesRequest { + // The query for deleting relationships + message Query { + // Optional. The namespace to query. + string namespace = 1; + // Optional. The object to query for. + string object = 2; + // Optional. The relation to query for. + string relation = 3; + // Optional. The subject to query for. + Subject subject = 4; + } + + Query query = 1 [deprecated = true]; + + RelationQuery relation_query = 2; +} + +message DeleteRelationTuplesResponse { + +} diff --git a/proto/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go b/proto/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go new file mode 100644 index 0000000..22cfc3c --- /dev/null +++ b/proto/ory/keto/relation_tuples/v1alpha2/write_service_grpc.pb.go @@ -0,0 +1,171 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ory/keto/relation_tuples/v1alpha2/write_service.proto + +package relationtuples + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + WriteService_TransactRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.WriteService/TransactRelationTuples" + WriteService_DeleteRelationTuples_FullMethodName = "/ory.keto.relation_tuples.v1alpha2.WriteService/DeleteRelationTuples" +) + +// WriteServiceClient is the client API for WriteService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// The write service to create and delete Access Control Lists. +// +// This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type WriteServiceClient interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(ctx context.Context, in *TransactRelationTuplesRequest, opts ...grpc.CallOption) (*TransactRelationTuplesResponse, error) + // Deletes relationships based on relation query + DeleteRelationTuples(ctx context.Context, in *DeleteRelationTuplesRequest, opts ...grpc.CallOption) (*DeleteRelationTuplesResponse, error) +} + +type writeServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWriteServiceClient(cc grpc.ClientConnInterface) WriteServiceClient { + return &writeServiceClient{cc} +} + +func (c *writeServiceClient) TransactRelationTuples(ctx context.Context, in *TransactRelationTuplesRequest, opts ...grpc.CallOption) (*TransactRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(TransactRelationTuplesResponse) + err := c.cc.Invoke(ctx, WriteService_TransactRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *writeServiceClient) DeleteRelationTuples(ctx context.Context, in *DeleteRelationTuplesRequest, opts ...grpc.CallOption) (*DeleteRelationTuplesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteRelationTuplesResponse) + err := c.cc.Invoke(ctx, WriteService_DeleteRelationTuples_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WriteServiceServer is the server API for WriteService service. +// All implementations must embed UnimplementedWriteServiceServer +// for forward compatibility. +// +// The write service to create and delete Access Control Lists. +// +// This service is part of the [write-APIs](../concepts/25_api-overview.mdx#write-apis). +type WriteServiceServer interface { + // Writes one or more relationships in a single transaction. + TransactRelationTuples(context.Context, *TransactRelationTuplesRequest) (*TransactRelationTuplesResponse, error) + // Deletes relationships based on relation query + DeleteRelationTuples(context.Context, *DeleteRelationTuplesRequest) (*DeleteRelationTuplesResponse, error) + mustEmbedUnimplementedWriteServiceServer() +} + +// UnimplementedWriteServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWriteServiceServer struct{} + +func (UnimplementedWriteServiceServer) TransactRelationTuples(context.Context, *TransactRelationTuplesRequest) (*TransactRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TransactRelationTuples not implemented") +} +func (UnimplementedWriteServiceServer) DeleteRelationTuples(context.Context, *DeleteRelationTuplesRequest) (*DeleteRelationTuplesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteRelationTuples not implemented") +} +func (UnimplementedWriteServiceServer) mustEmbedUnimplementedWriteServiceServer() {} +func (UnimplementedWriteServiceServer) testEmbeddedByValue() {} + +// UnsafeWriteServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WriteServiceServer will +// result in compilation errors. +type UnsafeWriteServiceServer interface { + mustEmbedUnimplementedWriteServiceServer() +} + +func RegisterWriteServiceServer(s grpc.ServiceRegistrar, srv WriteServiceServer) { + // If the following call panics, it indicates UnimplementedWriteServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WriteService_ServiceDesc, srv) +} + +func _WriteService_TransactRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TransactRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WriteServiceServer).TransactRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WriteService_TransactRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WriteServiceServer).TransactRelationTuples(ctx, req.(*TransactRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WriteService_DeleteRelationTuples_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteRelationTuplesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WriteServiceServer).DeleteRelationTuples(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WriteService_DeleteRelationTuples_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WriteServiceServer).DeleteRelationTuples(ctx, req.(*DeleteRelationTuplesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WriteService_ServiceDesc is the grpc.ServiceDesc for WriteService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WriteService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ory.keto.relation_tuples.v1alpha2.WriteService", + HandlerType: (*WriteServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TransactRelationTuples", + Handler: _WriteService_TransactRelationTuples_Handler, + }, + { + MethodName: "DeleteRelationTuples", + Handler: _WriteService_DeleteRelationTuples_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ory/keto/relation_tuples/v1alpha2/write_service.proto", +} diff --git a/schema/.snapshots/TestLexer-suite=snapshots-comments.json b/schema/.snapshots/TestLexer-suite=snapshots-comments.json new file mode 100644 index 0000000..a6e8540 --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-comments.json @@ -0,0 +1,13 @@ +[ + "/**/", + "/** doc comment\n * content\n * more content\n */", + "class", + "'name'", + "implements", + "'Namespace'", + "{", + "// Block comment", + "/*\n\täny ünicöde characterß???\n\t*/", + "}", + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-empty.json b/schema/.snapshots/TestLexer-suite=snapshots-empty.json new file mode 100644 index 0000000..118e568 --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-empty.json @@ -0,0 +1,3 @@ +[ + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-full_class.json b/schema/.snapshots/TestLexer-suite=snapshots-full_class.json new file mode 100644 index 0000000..ba05aea --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-full_class.json @@ -0,0 +1,142 @@ +[ + "class", + "'File'", + "implements", + "'Namespace'", + "{", + "'related'", + ":", + "{", + "'parents'", + ":", + "'File'", + "[", + "]", + "'viewers'", + ":", + "'User'", + "[", + "]", + "'owners'", + ":", + "'User'", + "[", + "]", + "'siblings'", + ":", + "'File'", + "[", + "]", + "}", + "'permits'", + "=", + "{", + "'view'", + ":", + "(", + "ctx", + ":", + "'Context'", + ")", + ":", + "'boolean'", + "=\u003e", + "this", + ".", + "'related'", + ".", + "'parents'", + ".", + "'some'", + "(", + "'p'", + "=\u003e", + "'p'", + ".", + "'permits'", + ".", + "'view'", + "(", + "ctx", + ")", + ")", + "||", + "this", + ".", + "'related'", + ".", + "'viewers'", + ".", + "'includes'", + "(", + "ctx", + ".", + "'subject'", + ")", + "||", + "!", + "this", + ".", + "'related'", + ".", + "'owners'", + ".", + "'includes'", + "(", + "ctx", + ".", + "'subject'", + ")", + ",", + "'edit'", + ":", + "(", + "ctx", + ":", + "'Context'", + ")", + "=\u003e", + "this", + ".", + "'related'", + ".", + "'owners'", + ".", + "'includes'", + "(", + "ctx", + ".", + "'subject'", + ")", + ",", + "'rename'", + ":", + "(", + "ctx", + ":", + "'Context'", + ")", + "=\u003e", + "this", + ".", + "'related'", + ".", + "'siblings'", + ".", + "'some'", + "(", + "'s'", + "=\u003e", + "'s'", + ".", + "'permits'", + ".", + "'edit'", + "(", + "ctx", + ")", + ")", + "}", + "}", + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-import_statement.json b/schema/.snapshots/TestLexer-suite=snapshots-import_statement.json new file mode 100644 index 0000000..4d561cf --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-import_statement.json @@ -0,0 +1,13 @@ +[ + "'import'", + "{", + "'Foo'", + ",", + "'Bar'", + ",", + "'Baz'", + "}", + "'from'", + "'@ory/keto-...'", + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-single_class.json b/schema/.snapshots/TestLexer-suite=snapshots-single_class.json new file mode 100644 index 0000000..edbce48 --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-single_class.json @@ -0,0 +1,16 @@ +[ + "class", + "'name'", + "implements", + "'Namespace'", + "{", + "'metadata'", + "=", + "{", + "'id'", + ":", + "'123'", + "}", + "}", + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-subject_sets.json b/schema/.snapshots/TestLexer-suite=snapshots-subject_sets.json new file mode 100644 index 0000000..d6bc004 --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-subject_sets.json @@ -0,0 +1,23 @@ +[ + "class", + "'X'", + "implements", + "'Namespace'", + "{", + "'related'", + ":", + "{", + "'relation'", + ":", + "'SubjectSet'", + "\u003c", + "'R'", + ",", + "'someProp'", + "\u003e", + "[", + "]", + "}", + "}", + "EOF" +] diff --git a/schema/.snapshots/TestLexer-suite=snapshots-two_classes.json b/schema/.snapshots/TestLexer-suite=snapshots-two_classes.json new file mode 100644 index 0000000..eb30c22 --- /dev/null +++ b/schema/.snapshots/TestLexer-suite=snapshots-two_classes.json @@ -0,0 +1,39 @@ +[ + "class", + "'user'", + "implements", + "'Namespace'", + "{", + "}", + "class", + "'document'", + "implements", + "'Namespace'", + "{", + "'related'", + ":", + "{", + "'owners'", + ":", + "'user'", + "[", + "]", + "'editors'", + ":", + "'user'", + "[", + "]", + "'viewers'", + ":", + "'user'", + "[", + "]", + "'parent'", + ":", + "'document'", + "[", + "]", + "}", + "}", + "EOF" +] diff --git a/schema/.snapshots/TestParser-suite=snapshots-advanced_typescript_syntax.json b/schema/.snapshots/TestParser-suite=snapshots-advanced_typescript_syntax.json new file mode 100644 index 0000000..8341340 --- /dev/null +++ b/schema/.snapshots/TestParser-suite=snapshots-advanced_typescript_syntax.json @@ -0,0 +1,161 @@ +{ + "Resource": [ + { + "name": "admins", + "types": [ + { + "namespace": "Role", + "relation": "member" + } + ] + }, + { + "name": "supervisors", + "types": [ + { + "namespace": "Role", + "relation": "member" + } + ] + }, + { + "name": "annotators", + "types": [ + { + "namespace": "Role", + "relation": "member" + } + ] + }, + { + "name": "medicalAnnotators", + "types": [ + { + "namespace": "Role", + "relation": "member" + } + ] + }, + { + "name": "read", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "admins", + "computed_subject_set_relation": "member" + }, + { + "relation": "annotators", + "computed_subject_set_relation": "member" + }, + { + "relation": "medicalAnnotators", + "computed_subject_set_relation": "member" + }, + { + "relation": "supervisors", + "computed_subject_set_relation": "member" + } + ] + } + }, + { + "name": "comment", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "read" + } + ] + } + }, + { + "name": "update", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "admins", + "computed_subject_set_relation": "member" + }, + { + "relation": "annotators", + "computed_subject_set_relation": "member" + }, + { + "relation": "medicalAnnotators", + "computed_subject_set_relation": "member" + }, + { + "relation": "supervisors", + "computed_subject_set_relation": "member" + } + ] + } + }, + { + "name": "create", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "admins", + "computed_subject_set_relation": "member" + }, + { + "relation": "annotators", + "computed_subject_set_relation": "member" + }, + { + "relation": "supervisors", + "computed_subject_set_relation": "member" + } + ] + } + }, + { + "name": "approve", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "admins", + "computed_subject_set_relation": "member" + }, + { + "relation": "supervisors", + "computed_subject_set_relation": "member" + } + ] + } + }, + { + "name": "delete", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "admins", + "computed_subject_set_relation": "member" + }, + { + "relation": "supervisors", + "computed_subject_set_relation": "member" + } + ] + } + } + ], + "Role": [ + { + "name": "member", + "types": [ + { + "namespace": "Role" + } + ] + } + ] +} diff --git a/schema/.snapshots/TestParser-suite=snapshots-full_example.json b/schema/.snapshots/TestParser-suite=snapshots-full_example.json new file mode 100644 index 0000000..4f02d95 --- /dev/null +++ b/schema/.snapshots/TestParser-suite=snapshots-full_example.json @@ -0,0 +1,169 @@ +{ + "File": [ + { + "name": "parents", + "types": [ + { + "namespace": "File" + }, + { + "namespace": "Folder" + } + ] + }, + { + "name": "viewers", + "types": [ + { + "namespace": "User" + }, + { + "namespace": "Group", + "relation": "members" + } + ] + }, + { + "name": "owners", + "types": [ + { + "namespace": "User" + }, + { + "namespace": "Group", + "relation": "members" + } + ] + }, + { + "name": "siblings", + "types": [ + { + "namespace": "File" + } + ] + }, + { + "name": "view", + "rewrite": { + "operator": "or", + "children": [ + { + "operator": "and", + "children": [ + { + "relation": "parents", + "computed_subject_set_relation": "viewers" + }, + { + "relation": "parents", + "computed_subject_set_relation": "view" + } + ] + }, + { + "relation": "viewers" + }, + { + "relation": "viewers" + }, + { + "relation": "viewers" + }, + { + "relation": "owners" + } + ] + } + }, + { + "name": "edit", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "owners" + } + ] + } + }, + { + "name": "not", + "rewrite": { + "operator": "or", + "children": [ + { + "inverted": { + "relation": "owners" + } + } + ] + } + }, + { + "name": "rename", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "siblings", + "computed_subject_set_relation": "edit" + } + ] + } + } + ], + "Folder": [ + { + "name": "parents", + "types": [ + { + "namespace": "File" + } + ] + }, + { + "name": "viewers", + "types": [ + { + "namespace": "Group", + "relation": "members" + } + ] + }, + { + "name": "view", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "viewers" + } + ] + } + } + ], + "Group": [ + { + "name": "members", + "types": [ + { + "namespace": "User" + }, + { + "namespace": "Group" + } + ] + } + ], + "User": [ + { + "name": "manager", + "types": [ + { + "namespace": "User" + } + ] + } + ] +} diff --git a/schema/.snapshots/TestParser-suite=snapshots-full_namespace.json b/schema/.snapshots/TestParser-suite=snapshots-full_namespace.json new file mode 100644 index 0000000..f12bad8 --- /dev/null +++ b/schema/.snapshots/TestParser-suite=snapshots-full_namespace.json @@ -0,0 +1,58 @@ +{ + "Document": [ + { + "name": "owners" + }, + { + "name": "editors" + }, + { + "name": "viewers" + }, + { + "name": "parent" + }, + { + "name": "rename", + "rewrite": { + "set_operation": 0, + "children": [ + { + "relation": "siblings", + "computed_userset_relation": "edit" + } + ] + } + }, + { + "name": "edit", + "rewrite": { + "set_operation": 0, + "children": [ + { + "relation": "owners" + } + ] + } + }, + { + "name": "view", + "rewrite": { + "set_operation": 0, + "children": [ + { + "relation": "parents", + "computed_userset_relation": "view" + }, + { + "relation": "viewers" + }, + { + "relation": "owners" + } + ] + } + } + ], + "User": null +} diff --git a/schema/.snapshots/TestParser-suite=snapshots-inverted_relationship_not_case.json b/schema/.snapshots/TestParser-suite=snapshots-inverted_relationship_not_case.json new file mode 100644 index 0000000..bf1fa3f --- /dev/null +++ b/schema/.snapshots/TestParser-suite=snapshots-inverted_relationship_not_case.json @@ -0,0 +1,55 @@ +{ + "File": [ + { + "name": "viewers", + "types": [ + { + "namespace": "User" + } + ] + }, + { + "name": "blocklist1", + "types": [ + { + "namespace": "User" + } + ] + }, + { + "name": "blocklist2", + "types": [ + { + "namespace": "User" + } + ] + }, + { + "name": "view", + "rewrite": { + "operator": "and", + "children": [ + { + "inverted": { + "relation": "viewers" + } + }, + { + "inverted": { + "operator": "and", + "children": [ + { + "relation": "blocklist1" + }, + { + "relation": "blocklist2" + } + ] + } + } + ] + } + } + ], + "User": null +} diff --git a/schema/.snapshots/TestParser-suite=snapshots-quoted_property_names.json b/schema/.snapshots/TestParser-suite=snapshots-quoted_property_names.json new file mode 100644 index 0000000..741ea20 --- /dev/null +++ b/schema/.snapshots/TestParser-suite=snapshots-quoted_property_names.json @@ -0,0 +1,47 @@ +{ + "Resource": [ + { + "name": "scope.relation", + "types": [ + { + "namespace": "Resource" + } + ] + }, + { + "name": "scope.action_0", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "scope.relation", + "computed_subject_set_relation": "scope.action_1" + } + ] + } + }, + { + "name": "scope.action_1", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "scope.relation", + "computed_subject_set_relation": "scope.relation" + } + ] + } + }, + { + "name": "scope.action_2", + "rewrite": { + "operator": "or", + "children": [ + { + "relation": "scope.action_0" + } + ] + } + } + ] +} diff --git a/schema/handler.go b/schema/handler.go new file mode 100644 index 0000000..448573f --- /dev/null +++ b/schema/handler.go @@ -0,0 +1,112 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "context" + "io" + "net/http" + + "connectrpc.com/connect" + "github.com/pkg/errors" + "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/ory/herodot" + "github.com/ory/x/httprouterx" + "github.com/ory/x/httpx" + "github.com/ory/x/logrusx" + + opl "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1" + "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1/oplconnect" + "github.com/ory/keto/internal/x" + "github.com/ory/keto/ketoapi" +) + +type ( + handlerDependencies interface { + logrusx.Provider + httpx.WriterProvider + x.HandlerOptionsProvider + } + Handler struct { + oplconnect.UnimplementedSyntaxServiceHandler + d handlerDependencies + } +) + +const RouteBase = "/opl/syntax/check" + +func NewHandler(d handlerDependencies) *Handler { + return &Handler{d: d} +} + +func (h *Handler) RegisterSyntaxRoutes(r httprouterx.Router) { + r.POST(RouteBase, h.postCheckOplSyntax) + r.Handle(oplconnect.NewSyntaxServiceHandler(h, h.d.HandlerOptions()...)) +} + +func (h *Handler) ProtoFiles() []protoreflect.FileDescriptor { + return []protoreflect.FileDescriptor{opl.File_ory_keto_opl_v1alpha1_syntax_service_proto} +} + +func (h *Handler) Check(_ context.Context, request *connect.Request[opl.CheckRequest]) (*connect.Response[opl.CheckResponse], error) { + _, parseErrors := Parse(string(request.Msg.GetContent())) + apiErrors := make([]*opl.ParseError, len(parseErrors)) + for i, e := range parseErrors { + apiErrors[i] = e.ToProto() + } + return connect.NewResponse(&opl.CheckResponse{ParseErrors: apiErrors}), nil +} + +// Check OPL Syntax Request Parameters +// +// swagger:parameters checkOplSyntax +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type checkOplSyntax struct { + // in: body + Body checkOplSyntaxBody +} + +// Ory Permission Language Document +// +// swagger:model checkOplSyntaxBody +// +//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions +type checkOplSyntaxBody string + +// swagger:route POST /opl/syntax/check relationship checkOplSyntax +// +// # Check the syntax of an OPL file +// +// The OPL file is expected in the body of the request. +// +// Consumes: +// - text/plain +// +// Produces: +// - application/json +// +// Schemes: http, https +// +// Responses: +// 200: checkOplSyntaxResult +// 400: errorGeneric +// default: errorGeneric +// +// Extensions: +// x-ory-ratelimit-bucket: keto-admin-medium +func (h *Handler) postCheckOplSyntax(w http.ResponseWriter, r *http.Request) { + data, err := io.ReadAll(r.Body) + if err != nil { + h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest().WithError(err.Error()))) + return + } + _, parseErrors := Parse(string(data)) + apiErrors := make([]*ketoapi.ParseError, len(parseErrors)) + for i, e := range parseErrors { + apiErrors[i] = e.ToAPI() + } + h.d.Writer().Write(w, r, &ketoapi.CheckOPLSyntaxResponse{Errors: apiErrors}) +} diff --git a/schema/handler_test.go b/schema/handler_test.go new file mode 100644 index 0000000..acec61b --- /dev/null +++ b/schema/handler_test.go @@ -0,0 +1,60 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema_test + +import ( + "bytes" + "io" + "net/http" + "testing" + + opl "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1" + "github.com/ory/keto/internal/driver" + "github.com/ory/keto/schema" + "github.com/ory/x/httprouterx" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + "google.golang.org/grpc" +) + +func TestNewHandler(t *testing.T) { + reg := driver.NewSqliteTestRegistry(t) + h := schema.NewHandler(reg) + + r := httprouterx.NewRouterPublic() + h.RegisterSyntaxRoutes(r) + + ts, clientTLS := driver.HTTP2TestServer(t, r) + + t.Run("proto=REST", func(t *testing.T) { + t.Run("method=POST /opl/syntax/check", func(t *testing.T) { + response, err := ts.Client().Post( + ts.URL+"/opl/syntax/check", + "text/plain", + bytes.NewBufferString("/* comment???")) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode) + body, err := io.ReadAll(response.Body) + require.NoError(t, err) + t.Log(string(body)) + assert.Contains(t, gjson.GetBytes(body, "errors.0.message").String(), "unclosed comment") + }) + }) + + t.Run("proto=gRPC", func(t *testing.T) { + conn, err := grpc.NewClient(ts.Listener.Addr().String(), grpc.WithTransportCredentials(clientTLS)) + require.NoError(t, err) + + client := opl.NewSyntaxServiceClient(conn) + + t.Run("method=Syntax.Check", func(t *testing.T) { + response, err := client.Check(t.Context(), &opl.CheckRequest{ + Content: []byte("/* comment???"), + }) + require.NoError(t, err) + assert.Contains(t, response.ParseErrors[0].Message, "unclosed comment") + }) + }) +} diff --git a/schema/itemtype_string.go b/schema/itemtype_string.go new file mode 100644 index 0000000..67bcb21 --- /dev/null +++ b/schema/itemtype_string.go @@ -0,0 +1,52 @@ +// Copyright © 2026 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by "stringer -type=itemType -trimprefix item -linecomment"; DO NOT EDIT. + +package schema + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[itemError-0] + _ = x[itemEOF-1] + _ = x[itemIdentifier-2] + _ = x[itemComment-3] + _ = x[itemStringLiteral-4] + _ = x[itemKeywordClass-5] + _ = x[itemKeywordImplements-6] + _ = x[itemKeywordThis-7] + _ = x[itemKeywordCtx-8] + _ = x[itemOperatorAnd-9] + _ = x[itemOperatorOr-10] + _ = x[itemOperatorNot-11] + _ = x[itemOperatorAssign-12] + _ = x[itemOperatorArrow-13] + _ = x[itemOperatorDot-14] + _ = x[itemOperatorColon-15] + _ = x[itemOperatorComma-16] + _ = x[itemSemicolon-17] + _ = x[itemTypeUnion-18] + _ = x[itemParenLeft-19] + _ = x[itemParenRight-20] + _ = x[itemBraceLeft-21] + _ = x[itemBraceRight-22] + _ = x[itemBracketLeft-23] + _ = x[itemBracketRight-24] + _ = x[itemAngledLeft-25] + _ = x[itemAngledRight-26] +} + +const _itemType_name = "ErrorEOFIdentifierCommentStringLiteralKeywordClassKeywordImplementsKeywordThisKeywordCtx\"&&\"\"||\"\"!\"\"=\"\"=>\"\".\"\":\"\",\"\";\"\"|\"\"(\"\")\"\"{\"\"}\"\"[\"\"]\"\"<\"\">\"" + +var _itemType_index = [...]uint8{0, 5, 8, 18, 25, 38, 50, 67, 78, 88, 92, 96, 99, 102, 106, 109, 112, 115, 118, 121, 124, 127, 130, 133, 136, 139, 142, 145} + +func (i itemType) String() string { + if i < 0 || i >= itemType(len(_itemType_index)-1) { + return "itemType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _itemType_name[_itemType_index[i]:_itemType_index[i+1]] +} diff --git a/schema/lexer.go b/schema/lexer.go new file mode 100644 index 0000000..96fbfd1 --- /dev/null +++ b/schema/lexer.go @@ -0,0 +1,341 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +// The lexer is inspired by Rob Pike's talk at +// https://www.youtube.com/watch?v=HxaD_trXwRE. + +package schema + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +type ( + item struct { + Typ itemType // Type of item. + Val string // Value of item. + Start, End int // Start and end position of item. + } + itemType int + + // stateFn represents the state of the scanner as a function that returns + // the next state. + stateFn func(*lexer) stateFn + + // lexer holds the state of the scanner. + lexer struct { + name string // the name of the input; used only for error reports. + input string // the string being scanned. + state stateFn // the next lexing function to enter + pos int // current position in the input. + start int // start position of this item. + width int // width of last rune read from input. + items chan item // channel of scanned items. + } +) + +//go:generate stringer -type=itemType -trimprefix item -linecomment +const ( + // error occurred; value is text of error + itemError itemType = iota + // end of input + itemEOF + + // identifier; value is string + itemIdentifier + // comment; value is string + itemComment + // string literal; value is string + itemStringLiteral + + // keywords + itemKeywordClass + itemKeywordImplements + itemKeywordThis + itemKeywordCtx + + // operators + itemOperatorAnd // "&&" + itemOperatorOr // "||" + itemOperatorNot // "!" + itemOperatorAssign // "=" + itemOperatorArrow // "=>" + itemOperatorDot // "." + itemOperatorColon // ":" + itemOperatorComma // "," + + // misc characters + itemSemicolon // ";" + itemTypeUnion // "|" + + // brackets + itemParenLeft // "(" + itemParenRight // ")" + itemBraceLeft // "{" + itemBraceRight // "}" + itemBracketLeft // "[" + itemBracketRight // "]" + itemAngledLeft // "<" + itemAngledRight // ">" +) + +// string classes +const ( + spaces = "\t\n\v\f\r " + digits = "0123456789" + letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_" +) + +const eof rune = -1 // rune indicating end of file + +func (i item) String() string { + switch i.Typ { + case itemError: + return "error: " + i.Val + case itemEOF: + return "EOF" + case itemIdentifier, itemStringLiteral: + if len(i.Val) > 10 { + return fmt.Sprintf("'%.10s...'", i.Val) + } + return fmt.Sprintf("'%s'", i.Val) + } + return i.Val +} + +// Lex creates a new scanner for the input string. +func Lex(name, input string) *lexer { + l := &lexer{ + name: name, + input: input, + state: lexCode, + items: make(chan item, 20), + } + return l +} + +// next returns the next rune in the input. +func (l *lexer) next() (r rune) { + if l.pos >= len(l.input) { + l.width = 0 + return eof + } + r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) + l.pos += l.width + return r +} + +// peek returns but does not consume the next rune in the input. +func (l *lexer) peek() rune { + r := l.next() + l.backup() + return r +} + +// backup steps back one rune. Can only be called once per call of next. +func (l *lexer) backup() { + l.pos -= l.width +} + +// emit passes an item back to the client. The value is cloned so that +// long-lived tokens do not pin the whole input in memory. +func (l *lexer) emit(t itemType) { + l.items <- item{t, strings.Clone(l.input[l.start:l.pos]), l.start, l.pos} + l.start = l.pos +} + +// ignore skips over the pending input before this point. +func (l *lexer) ignore() { + l.start = l.pos +} + +// accept consumes the next rune if it's from the valid set. +func (l *lexer) accept(valid string) bool { + if strings.ContainsRune(valid, l.next()) { + return true + } + l.backup() + return false +} + +// acceptRun consumes a run of runes from the valid set. +func (l *lexer) acceptRun(valid string) { + for strings.ContainsRune(valid, l.next()) { + } + l.backup() +} + +// error returns an error token and terminates the scan by passing +// back a nil pointer that will be the next state, terminating l.run. +func (l *lexer) errorf(format string, args ...interface{}) stateFn { + location := fmt.Sprintf("at %q: ", l.input[l.pos:]) + l.items <- item{itemError, location + fmt.Sprintf(format, args...), l.start, l.pos} + return nil +} + +// nextItem returns the next item from the input. +func (l *lexer) nextItem() item { + for { + select { + case item := <-l.items: + return item + default: + if l.state == nil { + return item{itemError, "broken state", 0, 0} + } + l.state = l.state(l) + } + } +} + +// nextNonCommentItem returns the next item from the input that is not a comment. +func (l *lexer) nextNonCommentItem() (item item) { + for item = l.nextItem(); item.Typ == itemComment; item = l.nextItem() { + } + return +} + +// scanIdentifier scans an identifier. +func (l *lexer) scanIdentifier() bool { + if !l.accept(letters) { + return false + } + l.acceptRun(letters + digits) + return true +} + +func (l *lexer) scanCommentBegin() (bool, stateFn) { + if strings.HasPrefix(l.input[l.pos:], "//") { + l.pos += 2 + return true, lexLineComment + } + if strings.HasPrefix(l.input[l.pos:], "/*") { + l.pos += 2 + return true, lexBlockComment + } + return false, nil +} + +var oneRuneTokens = map[rune]itemType{ + ':': itemOperatorColon, + '.': itemOperatorDot, + '(': itemParenLeft, + ')': itemParenRight, + '[': itemBracketLeft, + ']': itemBracketRight, + '{': itemBraceLeft, + '}': itemBraceRight, + '<': itemAngledLeft, + '>': itemAngledRight, + '=': itemOperatorAssign, + ',': itemOperatorComma, + ';': itemSemicolon, + '|': itemTypeUnion, + '!': itemOperatorNot, +} + +var multiRuneTokens = map[string]itemType{ + "=>": itemOperatorArrow, + "||": itemOperatorOr, + "&&": itemOperatorAnd, +} + +var keywords = map[string]itemType{ + "class": itemKeywordClass, + "implements": itemKeywordImplements, + "this": itemKeywordThis, + "ctx": itemKeywordCtx, +} + +func lexCode(l *lexer) stateFn { + l.acceptRun(spaces) + l.ignore() + + r := l.peek() + if r == eof { + l.emit(itemEOF) + return nil + } + + // Two-rune tokens must be matched first + for token, typ := range multiRuneTokens { + if strings.HasPrefix(l.input[l.pos:], token) { + l.pos += len(token) + l.emit(typ) + return lexCode + } + } + + if found, lexComment := l.scanCommentBegin(); found { + return lexComment + } + + if itemType, found := oneRuneTokens[r]; found { + l.next() + l.emit(itemType) + return lexCode + } + + if strings.ContainsRune(`'"`, r) { + return lexStringLiteral + } + + if l.scanIdentifier() { + if kwType, found := keywords[l.input[l.start:l.pos]]; found { + l.emit(kwType) + } else { + l.emit(itemIdentifier) + } + return lexCode + } + + return l.errorf("unexpected token %c", r) +} + +func lexLineComment(l *lexer) stateFn { + for { + switch l.next() { + case '\n', eof: + l.backup() + l.emit(itemComment) + return lexCode + } + } +} + +func lexBlockComment(l *lexer) stateFn { + for r := l.peek(); ; r = l.next() { + if r == eof { + return l.errorf("unclosed comment") + } + if strings.HasPrefix(l.input[l.pos:], "*/") { + l.pos += 2 + l.emit(itemComment) + return lexCode + } + } +} + +// lexStringLiteral scans a string literal. +func lexStringLiteral(l *lexer) stateFn { + r := l.next() + l.ignore() + +loop: + for { + switch l.next() { + case eof: + return l.errorf("unclosed string literal") + case r: + l.backup() + break loop + } + } + l.emit(itemStringLiteral) + l.next() + l.ignore() + + return lexCode +} diff --git a/schema/lexer_test.go b/schema/lexer_test.go new file mode 100644 index 0000000..0967021 --- /dev/null +++ b/schema/lexer_test.go @@ -0,0 +1,133 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "strings" + "testing" + + "github.com/ory/x/snapshotx" +) + +var lexingErrorTestCases = []struct{ name, input string }{ + {"open comment", "/*"}, + {"open string literal", "'"}, + {"non-token", "ü"}, + {"mixed quotes", `"invalid_literal'`}, +} + +var lexableTestCases = []struct{ name, input string }{ + {"empty", ""}, + {"import statement", "import { Foo, Bar, Baz } from '@ory/keto-namespace-types'"}, + {"single class", ` +class name implements Namespace { + metadata = { + id: "123" + } +} +`}, + {"comments", ` +/**/ + +/** doc comment + * content + * more content + */ +class name implements Namespace { + // Block comment + /* + äny ünicöde characterß??? + */ +} +`}, + {"two classes", ` +class user implements Namespace { } + +class document implements Namespace { + + related: { + owners: user[] + editors: user[] + viewers: user[] + parent: document[] + } +} +`}, + {"full class", ` +class File implements Namespace { + related: { + parents: File[] + viewers: User[] + owners: User[] + siblings: File[] + } + + permits = { + view: (ctx: Context): boolean => + this.related.parents.some(p => p.permits.view(ctx)) || + this.related.viewers.includes(ctx.subject) || + !this.related.owners.includes(ctx.subject), + + edit: (ctx: Context) => this.related.owners.includes(ctx.subject), + + rename: (ctx: Context) => this.related.siblings.some(s => s.permits.edit(ctx)) + } +} +`}, + {"subject sets", ` +class X implements Namespace { + related: { + relation: SubjectSet[] + } +} +`}, +} + +func TestLexer(t *testing.T) { + t.Run("suite=snapshots", func(t *testing.T) { + for _, tc := range lexableTestCases { + t.Run(tc.name, func(t *testing.T) { + l := Lex(tc.name, tc.input) + var items []string + for { + item := l.nextItem() + items = append(items, item.String()) + + if item.Typ == itemError { + t.Fail() + break + } + if item.Typ == itemEOF { + break + } + } + t.Logf("Tokens:\n%s", strings.Join(items, "\n")) + snapshotx.SnapshotT(t, items) + }) + } + }) + + t.Run("suite=errors", func(t *testing.T) { + for _, tc := range lexingErrorTestCases { + t.Run(tc.name, func(t *testing.T) { + l := Lex(tc.name, tc.input) + var items []string + for { + item := l.nextItem() + items = append(items, item.String()) + + if item.Typ == itemError { + break + } + if item.Typ == itemEOF { + t.Fatal("reached EOF, but expected error") + } + } + l.next() + t.Logf("Tokens:\n%s", strings.Join(items, "\n")) + }) + } + + }) +} diff --git a/schema/limits.go b/schema/limits.go new file mode 100644 index 0000000..96a70a4 --- /dev/null +++ b/schema/limits.go @@ -0,0 +1,14 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +const ( + // tupleToSubjectSetTypeCheckMaxDepth controls the maximum number of recursions + // for looking up the types of SubjectSet. + tupleToSubjectSetTypeCheckMaxDepth = 10 + + // expressionNestingMaxDepth is the maximum number of nested '(' and '!' in + // a single 'permits'. + expressionNestingMaxDepth = 10 +) diff --git a/schema/parse_errors.go b/schema/parse_errors.go new file mode 100644 index 0000000..4ccb68e --- /dev/null +++ b/schema/parse_errors.go @@ -0,0 +1,129 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "fmt" + "math" + "strings" + "unicode" + + opl "github.com/ory/keto/gen/go/ory/keto/opl/v1alpha1" + "github.com/ory/keto/ketoapi" +) + +type ( + ParseError struct { + msg string + item item + p *parser + } +) + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func (e *ParseError) Error() string { + var s strings.Builder + start := e.toSrcPos(e.item.Start) + end := e.toSrcPos(e.item.End) + rows := e.rows() + startLineIdx := max(start.Line-2, 0) + errorLineIdx := max(start.Line-1, 0) + + _, _ = fmt.Fprintf(&s, "error from %d:%d to %d:%d: %s\n\n", + start.Line, start.Col, + end.Line, end.Col, + e.msg) + + if len(rows) < start.Line { + s.WriteString("meta error: could not find source position in input\n") + return s.String() + } + + for line := startLineIdx; line <= errorLineIdx; line++ { + _, _ = fmt.Fprintf(&s, "%4d | %s\n", line, rows[line]) + } + s.WriteString(" | ") + for i, r := range rows[errorLineIdx] { + switch { + case start.Col == i: + s.WriteRune('^') + case start.Col <= i && i <= end.Col-1: + s.WriteRune('~') + case unicode.IsSpace(r): + s.WriteRune(r) + default: + s.WriteRune(' ') + } + } + s.WriteRune('\n') + + if errorLineIdx+1 < len(rows) { + _, _ = fmt.Fprintf(&s, "%4d | %s\n", errorLineIdx+1, rows[errorLineIdx+1]) + s.WriteRune('\n') + } + + return s.String() +} + +func (e *ParseError) ToAPI() *ketoapi.ParseError { + return &ketoapi.ParseError{ + Message: e.msg, + Start: e.toSrcPos(e.item.Start), + End: e.toSrcPos(e.item.End), + } +} +func (e *ParseError) ToProto() *opl.ParseError { + start := e.toSrcPos(e.item.Start) + end := e.toSrcPos(e.item.End) + return &opl.ParseError{ + Message: e.msg, + Start: &opl.SourcePosition{ + Line: clampUint32(start.Line), + Column: clampUint32(start.Col), + }, + End: &opl.SourcePosition{ + Line: clampUint32(end.Line), + Column: clampUint32(end.Col), + }, + } +} + +// toSrcPos converts the given position in the input to a Line and column +// number. +func (e *ParseError) toSrcPos(pos int) (srcPos ketoapi.SourcePosition) { + srcPos.Line = 1 + for _, c := range e.p.lexer.input { + srcPos.Col++ + pos-- + if pos <= 0 { + break + } + if c == '\n' { + srcPos.Line++ + srcPos.Col = 0 + } + } + return +} + +func clampUint32(v int) uint32 { + switch { + case v < 0: + return 0 + case uint64(v) > math.MaxUint32: + return math.MaxUint32 + default: + return uint32(v) + } +} + +func (e *ParseError) rows() []string { + return strings.Split(e.p.lexer.input, "\n") +} diff --git a/schema/parser.go b/schema/parser.go new file mode 100644 index 0000000..4c90efc --- /dev/null +++ b/schema/parser.go @@ -0,0 +1,550 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "fmt" + + internalNamespace "github.com/ory/keto/internal/namespace" + "github.com/ory/keto/internal/namespace/ast" +) + +type ( + namespace = internalNamespace.Namespace + + parser struct { + lexer *lexer // lexer to get tokens from + namespaces []namespace // list of parsed namespaces + namespace namespace // current namespace + errors []*ParseError // errors encountered during parsing + fatal bool // parser encountered a fatal error + lookahead *item // lookahead token + checks []typeCheck // checks to perform on the namespace + } +) + +func Parse(input string) ([]namespace, []*ParseError) { + p := &parser{ + lexer: Lex("input", input), + } + return p.parse() +} + +func (p *parser) next() (item item) { + if p.lookahead != nil { + item = *p.lookahead + p.lookahead = nil + } else { + return p.lexer.nextNonCommentItem() + } + return +} + +func (p *parser) peek() item { + if p.lookahead == nil { + i := p.lexer.nextNonCommentItem() + p.lookahead = &i + return i + } + return *p.lookahead +} + +func (p *parser) parse() ([]namespace, []*ParseError) { +loop: + for !p.fatal { + switch item := p.next(); item.Typ { + case itemEOF: + break loop + case itemError: + p.addFatal(item, "fatal: %s", item.Val) + case itemKeywordClass: + p.parseClass() + } + } + + if len(p.errors) == 0 { + p.typeCheck() + } + + return p.namespaces, p.errors +} + +func (p *parser) addFatal(item item, format string, a ...interface{}) { + p.addErr(item, format, a...) + p.fatal = true +} + +func (p *parser) addErr(item item, format string, a ...interface{}) { + err := &ParseError{ + msg: fmt.Sprintf(format, a...), + item: item, + p: p, + } + p.errors = append(p.errors, err) +} + +type matcher func(p *parser) (matched bool) + +// optional optionally matches the first argument of tokens in the input. If +// matched, the tokens are consumed. If the first token matched, all other +// tokens must match as well. +func optional(tokens ...string) matcher { + return func(p *parser) bool { + if len(tokens) == 0 { + return true + } + first := tokens[0] + if p.peek().Val == first { + p.next() + for _, token := range tokens[1:] { + i := p.next() + if i.Val != token { + p.addFatal(i, "expected %q, got %q", token, i.Val) + return false + } + } + } + return true + } +} + +// match for the next tokens in the input. +// +// A token is matched depending on the type: +// For string arguments, the input token must match the given string exactly. +// For *string arguments, the input token must be an identifier, and the value +// of the identifier will be written to the *string. +// For *item arguments, the input token will be written to the pointer. +func (p *parser) match(tokens ...interface{}) (matched bool) { + if p.fatal { + return false + } + + for _, token := range tokens { + switch token := token.(type) { + case string: + i := p.next() + if i.Val != token { + p.addFatal(i, "expected %q, got %q", token, i.Val) + return false + } + case *string: + i := p.next() + if i.Typ != itemIdentifier && i.Typ != itemStringLiteral { + p.addFatal(i, "expected identifier, got %s", i.Typ) + return false + } + *token = i.Val + case *item: + *token = p.next() + case matcher: + if !token(p) { + return false + } + default: + panic(fmt.Sprintf("unexpected token type %T", token)) + } + } + return true +} + +type itemPredicate func(item) bool + +func is(typ itemType) itemPredicate { + return func(item item) bool { + return item.Typ == typ + } +} + +// matchIf matches the tokens iff. the predicate is true. +func (p *parser) matchIf(predicate itemPredicate, tokens ...interface{}) (matched bool) { + if p.fatal { + return false + } + if !predicate(p.peek()) { + return false + } + return p.match(tokens...) +} + +// parseClass parses a class. The "class" token was already consumed. +func (p *parser) parseClass() { + var name string + p.match(&name, "implements", "Namespace", "{") + p.namespace = namespace{Name: name} + + for !p.fatal { + switch item := p.next(); { + case item.Typ == itemBraceRight: + p.namespaces = append(p.namespaces, p.namespace) + return + case item.Val == "related": + p.parseRelated() + case item.Val == "permits": + p.parsePermits() + case item.Typ == itemSemicolon: + continue + default: + p.addFatal(item, "expected 'permits' or 'related', got %q", item.Val) + return + } + } +} + +func (p *parser) parseRelated() { + p.match(":", "{") + for !p.fatal { + switch item := p.next(); item.Typ { + + case itemSemicolon: + continue + + case itemBraceRight: + return + + case itemIdentifier, itemStringLiteral: + relation := item.Val + var types []ast.RelationType + p.match(":") + + switch item := p.next(); { + case item.Val == "Array": + p.match("<") + types = append(types, p.parseTypeUnion(itemAngledRight)...) + case item.Val == "SubjectSet": + types = append(types, p.matchSubjectSet()) + p.match("[", "]", optional(",")) + case item.Typ == itemParenLeft: + types = append(types, p.parseTypeUnion(itemParenRight)...) + p.match("[", "]", optional(",")) + default: + types = append(types, ast.RelationType{Namespace: item.Val}) + p.addCheck(checkNamespaceExists(item)) + p.match("[", "]", optional(",")) + } + + p.namespace.Relations = append(p.namespace.Relations, ast.Relation{ + Name: relation, + Types: types, + }) + + default: + p.addFatal(item, "expected identifier or '}', got %s %q", item.Typ.String(), item.Val) + return + } + } +} + +func (p *parser) matchSubjectSet() ast.RelationType { + var namespace, relation item + p.match("<", &namespace, ",", &relation, ">") + p.addCheck(checkNamespaceHasRelation(namespace, relation)) + return ast.RelationType{Namespace: namespace.Val, Relation: relation.Val} +} + +func (p *parser) parseTypeUnion(endToken itemType) (types []ast.RelationType) { + for !p.fatal { + var identifier item + p.match(&identifier) + if identifier.Val == "SubjectSet" { + types = append(types, p.matchSubjectSet()) + } else { + types = append(types, ast.RelationType{Namespace: identifier.Val}) + p.addCheck(checkNamespaceExists(identifier)) + } + switch item := p.next(); item.Typ { + case endToken: + return + case itemTypeUnion: + default: + p.addFatal(item, "expected '|', got %q", item.Val) + } + } + return +} + +func (p *parser) parsePermits() { + p.match("=", "{") + for !p.fatal { + switch item := p.next(); item.Typ { + + case itemBraceRight: + return + + case itemIdentifier, itemStringLiteral: + permission := item.Val + p.match( + ":", "(", "ctx", optional(":", "Context"), ")", + optional(":", "boolean"), "=>", + ) + + rewrite := simplifyExpression(p.parsePermissionExpressions(itemOperatorComma, expressionNestingMaxDepth)) + if rewrite == nil { + return + } + p.namespace.Relations = append(p.namespace.Relations, + ast.Relation{ + Name: permission, + SubjectSetRewrite: rewrite, + }) + + default: + p.addFatal(item, "expected identifier or '}', got %s %q", item.Typ.String(), item.Val) + return + } + } +} + +func (p *parser) parsePermissionExpressions(finalToken itemType, depth int) *ast.SubjectSetRewrite { + if depth <= 0 { + p.addFatal(p.peek(), + "expression nested too deeply; maximal nesting depth is %d", + expressionNestingMaxDepth) + return nil + } + var root *ast.SubjectSetRewrite + + // We only expect an expression in the beginning and after a binary + // operator. + expectExpression := true + + // TODO(hperl): Split this into two state machines: One that parses an + // expression or expression group; and one that parses a binary operator. + for !p.fatal { + switch item := p.peek(); item.Typ { + + // A "(" starts a new expression group that is parsed recursively. + case itemParenLeft: + p.next() // consume paren + child := p.parsePermissionExpressions(itemParenRight, depth-1) + if child == nil { + return nil + } + root = addChild(root, child) + expectExpression = false + + case finalToken: + p.next() // consume final token + return root + + case itemBraceRight: + // We don't consume the '}' here, to allow `parsePermits` to consume + // it. + return root + + case itemOperatorAnd, itemOperatorOr: + p.next() // consume operator + + // A nil root means that we saw a binary expression before the first + // expression. + if root == nil { + return nil + } + newRoot := &ast.SubjectSetRewrite{ + Operation: setOperation(item.Typ), + Children: []ast.Child{root}, + } + root = newRoot + expectExpression = true + + // A "not" creates an AST node where the children are either a + // single expression, or a list of expressions grouped by "()". + case itemOperatorNot: + p.next() // consume operator + child := p.parseNotExpression(depth - 1) + if child == nil { + return nil + } + root = addChild(root, child) + expectExpression = false + + default: + if !expectExpression { + // Two expressions can't follow each other directly, they must + // be separated by a binary operator. + p.addFatal(item, "did not expect another expression") + return nil + } + child := p.parsePermissionExpression() + if child == nil { + return nil + } + root = addChild(root, child) + expectExpression = true + } + } + return nil +} + +func (p *parser) parseNotExpression(depth int) ast.Child { + if depth <= 0 { + p.addFatal(p.peek(), + "expression nested too deeply; maximal nesting depth is %d", + expressionNestingMaxDepth) + return nil + } + + var child ast.Child + if item := p.peek(); item.Typ == itemParenLeft { + p.next() // consume paren + child = p.parsePermissionExpressions(itemParenRight, depth-1) + } else { + child = p.parsePermissionExpression() + } + if child == nil { + return nil + } + return &ast.InvertResult{Child: child} +} + +func addChild(root *ast.SubjectSetRewrite, child ast.Child) *ast.SubjectSetRewrite { + if root == nil { + return child.AsRewrite() + } else { + root.Children = append(root.Children, child) + return root + } +} + +func setOperation(typ itemType) ast.Operator { + switch typ { + case itemOperatorAnd: + return ast.OperatorAnd + case itemOperatorOr: + return ast.OperatorOr + } + panic("not reached") +} + +func (p *parser) matchPropertyAccess(propertyName any) bool { + return p.matchIf(is(itemBracketLeft), "[", propertyName, "]") || p.match(".", propertyName) +} + +func (p *parser) parsePermissionExpression() (child ast.Child) { + var name, verb item + + if !p.match("this", ".", &verb) { + return + } + if !p.matchPropertyAccess(&name) { + return + } + + switch verb.Val { + case "related": + if !p.match(".") { + return + } + switch item := p.next(); item.Val { + case "traverse": + child = p.parseTupleToSubjectSet(name) + case "includes": + child = p.parseComputedSubjectSet(name) + default: + p.addFatal(item, "expected 'traverse' or 'includes', got %q", item.Val) + } + + case "permits": + if !p.match("(", "ctx", ")") { + return + } + p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, name)) + return &ast.ComputedSubjectSet{Relation: name.Val} + + default: + p.addFatal(verb, "expected 'related' or 'permits', got %q", verb.Val) + } + + return +} + +func (p *parser) parseTupleToSubjectSet(relation item) (rewrite ast.Child) { + var ( + subjectSetRel string + arg, verb item + ) + if !p.match("(") { + return nil + } + + switch { + case p.matchIf(is(itemParenLeft), "(", &arg, ")"): + case p.match(&arg): + default: + return nil + } + p.match("=>", arg.Val, ".", &verb) + + switch verb.Val { + case "related": + if !p.matchPropertyAccess(&subjectSetRel) { + return nil + } + p.match( + ".", "includes", "(", "ctx", ".", "subject", + optional(","), ")", optional(","), ")", + ) + p.addCheck(checkAllRelationsTypesHaveRelation( + &p.namespace, relation, subjectSetRel, + )) + case "permits": + if !p.matchPropertyAccess(&subjectSetRel) { + return nil + } + p.match("(", "ctx", ")", ")") + p.addCheck(checkAllRelationsTypesHaveRelation( + &p.namespace, relation, subjectSetRel, + )) + default: + p.addFatal(verb, "expected 'related' or 'permits', got %q", verb) + return nil + } + p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation)) + return &ast.TupleToSubjectSet{ + Relation: relation.Val, + ComputedSubjectSetRelation: subjectSetRel, + } +} + +func (p *parser) parseComputedSubjectSet(relation item) (rewrite ast.Child) { + if !p.match("(", "ctx", ".", "subject", ")") { + return nil + } + p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation)) + return &ast.ComputedSubjectSet{Relation: relation.Val} +} + +// simplifyExpression rewrites the expression to use n-ary set operations +// instead of binary ones. +func simplifyExpression(root *ast.SubjectSetRewrite) *ast.SubjectSetRewrite { + if root == nil { + return nil + } + var newChildren []ast.Child + for _, child := range root.Children { + switch ch := child.(type) { + case *ast.SubjectSetRewrite: + simplifyExpression(ch) + if len(ch.Children) == 1 || ch.Operation == root.Operation { + // Flatten the child to parent, when: + // - it's a single-element wrapper (AND(OR(A)) ≡ AND(A)) + // - shares the root's operation (OR(OR(A,B),C) ≡ OR(A,B,C)). + newChildren = append(newChildren, ch.Children...) + continue + } + newChildren = append(newChildren, child) + case *ast.InvertResult: + if innerCh, ok := ch.Child.(*ast.SubjectSetRewrite); ok { + simplifyExpression(innerCh) + } + newChildren = append(newChildren, child) + default: + // Leaf node or different operation with multiple children; keep as-is. + newChildren = append(newChildren, child) + } + } + root.Children = newChildren + + return root +} diff --git a/schema/parser_fuzzer.go b/schema/parser_fuzzer.go new file mode 100644 index 0000000..d2cf834 --- /dev/null +++ b/schema/parser_fuzzer.go @@ -0,0 +1,9 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +func LibfuzzerFuzzParser(data []byte) int { + Parse(string(data)) + return 0 +} diff --git a/schema/parser_test.go b/schema/parser_test.go new file mode 100644 index 0000000..c3e4463 --- /dev/null +++ b/schema/parser_test.go @@ -0,0 +1,421 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import ( + "testing" + + "github.com/ory/x/snapshotx" + "github.com/stretchr/testify/assert" + + "github.com/ory/keto/internal/namespace/ast" +) + +var parserErrorTestCases = []struct{ name, input string }{ + {"lexer error", "/* unclosed comment"}, + { + "syntax and type errors", + ` + class File implements Namespace { + related: { + parents: (File | Folder)[] + viewers: (User | SubjectSet)[] + owners: (User | SubjectSet)[] + siblings: File[] + } + + SYNTAX ERROR + + // Some comment + permits = { + view: (ctx: Context): boolean => + ( + this.related.parents.traverse((p) => + p.related.viewers.includes(ctx.subject), + ) && + this.related.parents.traverse(p => p.permits.view(ctx)) ) || + (this.related.viewers.includes(ctx.subject) || + this.related.viewers.includes(ctx.subject) || + this.related.viewers.includes(ctx.subject) ) || + this.related.owners.includes(ctx.subject), + + edit: (ctx: Context) => this.related.owners.includes(ctx.subject), + + not: (ctx: Context) => !this.related.owners.includes(ctx.subject), + + rename: (ctx: Context) => + this.related.siblings.traverse(s => s.permits.edit(ctx)), + } + } +`, + }, + {"parser error", ` +class Resource implements Namespace { + permits = { + update: (ctx: Context) => || + this.related.annotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), +`}, +} + +var parserTestCases = []struct { + name, input string +}{ + {"full example", ` + import { Namespace, SubjectSet, FooBar, Anything } from '@ory/keto-namespace-types' + + class User implements Namespace { + related: { + manager: User[]; + } + } + + class Group implements Namespace { + related: { + members: (User | Group)[]; + }; + } + + class Folder implements Namespace { + related: { + parents: Array + viewers: Array> + } + + permits = { + view: (ctx: Context): boolean => this.related.viewers.includes(ctx.subject), + } + } + + class File implements Namespace { + related: { + parents: Array + viewers: (User | SubjectSet)[] + "owners": (User | SubjectSet)[] + siblings: File[] + } + + // Some comment + permits = { + view: (ctx: Context): boolean => + ( + this.related.parents.traverse((p) /* comment */ => + p.related.viewers.includes(ctx.subject), + ) && // comment + this.related.parents.traverse(p => p.permits.view(ctx)) ) || + (this.related.viewers.includes(ctx.subject) || // some comment + this.related.viewers.includes(ctx.subject) || /* another comment */ + this.related.viewers.includes(ctx.subject) ) || + this.related.owners.includes(ctx.subject), + + 'edit': (ctx: Context) => this.related.owners.includes(ctx.subject), + + not: (ctx: Context) => !this.related.owners.includes(ctx.subject), + + rename: (ctx: Context) => + this.related.siblings.traverse(s => s.permits.edit(ctx)), + } + } +`}, + { + "advanced typescript syntax", + ` +import { Namespace, SubjectSet, Context } from '@ory/keto-namespace-types'; + +class Role implements Namespace { + related: { + member: Role[] + } +} + +class Resource implements Namespace { + related: { + admins: SubjectSet[], + supervisors: SubjectSet[], + annotators: SubjectSet[], + medicalAnnotators: SubjectSet[], + }; + + permits = { + read: (ctx: Context) => this.related.admins.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.annotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.medicalAnnotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), + + comment: (ctx: Context) => this.permits.read(ctx), + + update: (ctx: Context) => this.related.admins.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.annotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.medicalAnnotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), + + create: (ctx: Context) => this.related.admins.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.annotators.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), + + approve: (ctx: Context) => this.related.admins.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), + + delete: (ctx: Context) => this.related.admins.traverse((role) => role.related.member.includes(ctx.subject)) || + this.related.supervisors.traverse((role) => role.related.member.includes(ctx.subject)), + }; +} +`, + }, + {"quoted property names", ` +class Resource implements Namespace { + related: { + "scope.relation": Resource[] + } + permits = { + "scope.action_0": (ctx: Context) => this.related["scope.relation"].traverse((r) => r.permits["scope.action_1"](ctx)), + "scope.action_1": (ctx: Context) => this.related["scope.relation"].traverse((r) => r.related["scope.relation"].includes(ctx.subject)), + "scope.action_2": (ctx: Context) => this.permits["scope.action_0"](ctx), + } +}`}, + {"inverted relationship not case", ` + class User implements Namespace{} + class File implements Namespace{ + related: { + viewers: User[] + blocklist1: User[] + blocklist2: User[] + } + permits = { + view: (ctx: Context) => + !this.related.viewers.includes(ctx.subject) && + (!( + this.related.blocklist1.includes(ctx.subject) && + this.related.blocklist2.includes(ctx.subject) + )) + } + } +`}, +} + +func TestParser(t *testing.T) { + t.Run("suite=snapshots", func(t *testing.T) { + for _, tc := range parserTestCases { + t.Run(tc.name, func(t *testing.T) { + ns, errs := Parse(tc.input) + if len(errs) > 0 { + for _, err := range errs { + t.Error(err) + } + } + t.Logf("namespaces:\n%+v", ns) + nsMap := make(map[string][]ast.Relation) + for _, n := range ns { + nsMap[n.Name] = n.Relations + } + snapshotx.SnapshotT(t, nsMap) + }) + } + }) + + t.Run("suite=errors", func(t *testing.T) { + for _, tc := range parserErrorTestCases { + t.Run(tc.name, func(t *testing.T) { + _, errs := Parse(tc.input) + assert.Len(t, errs, 1) + }) + } + }) +} + +func FuzzParser(f *testing.F) { + for _, tc := range lexableTestCases { + f.Add(tc.input) + } + for _, tc := range parserTestCases { + f.Add(tc.input) + } + + f.Fuzz(func(_ *testing.T, input string) { + Parse(input) + }) +} + +func Test_simplify(t *testing.T) { + testCases := []struct { + name string + input, expected *ast.SubjectSetRewrite + }{ + {"empty", nil, nil}, + { + name: "merge all unions", + input: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.SubjectSetRewrite{ + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + &ast.SubjectSetRewrite{ + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "C"}, + &ast.SubjectSetRewrite{ + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "D"}, + }, + }, + }, + }, + }, + }, + }, + }, + expected: &ast.SubjectSetRewrite{ + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.ComputedSubjectSet{Relation: "B"}, + &ast.ComputedSubjectSet{Relation: "C"}, + &ast.ComputedSubjectSet{Relation: "D"}, + }, + }, + }, + { + // ( ( (A || B) ) ) = A || B + name: "merges OR children", + input: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + }, + }, + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + }, + expected: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + { + // ( ( A && ((B)) ) ) = A && B + name: "merges AND children", + input: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + }, + }, + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + }, + }, + }, + expected: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + { + // A AND !!B = A AND !!B + name: "preserve double NOT", + input: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.InvertResult{ + Child: &ast.InvertResult{ + Child: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + }, + }, + }, + expected: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.InvertResult{ + Child: &ast.InvertResult{ + Child: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + }, + }, + }, + }, + }, + }, + }, + { + // A AND NOT(B OR (C)) -> A AND NOT(B OR C) + name: "simplifies the expression inside the inverted relationship", + input: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.InvertResult{ + Child: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "C"}, + }, + }, + }, + }, + }, + }, + }, + expected: &ast.SubjectSetRewrite{ + Operation: ast.OperatorAnd, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "A"}, + &ast.InvertResult{ + Child: &ast.SubjectSetRewrite{ + Operation: ast.OperatorOr, + Children: ast.Children{ + &ast.ComputedSubjectSet{Relation: "B"}, + &ast.ComputedSubjectSet{Relation: "C"}, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, simplifyExpression(tc.input)) + }) + } +} diff --git a/schema/testdata/fuzz/FuzzParser/7bf9b1cee2e0d42eec4de5897ecd6c79155329c5fd7cb26467a682eb780bbe94 b/schema/testdata/fuzz/FuzzParser/7bf9b1cee2e0d42eec4de5897ecd6c79155329c5fd7cb26467a682eb780bbe94 new file mode 100644 index 0000000..1dad7dd --- /dev/null +++ b/schema/testdata/fuzz/FuzzParser/7bf9b1cee2e0d42eec4de5897ecd6c79155329c5fd7cb26467a682eb780bbe94 @@ -0,0 +1,2 @@ +go test fuzz v1 +string("class A implements Namespace{permits={A:(ctx)=>||") diff --git a/schema/testdata/fuzz/FuzzParser/f094e0a70d8ff635f81c036b0a06464b14d2dda6f853748e524a6ca1f39ced0b b/schema/testdata/fuzz/FuzzParser/f094e0a70d8ff635f81c036b0a06464b14d2dda6f853748e524a6ca1f39ced0b new file mode 100644 index 0000000..49ed78b --- /dev/null +++ b/schema/testdata/fuzz/FuzzParser/f094e0a70d8ff635f81c036b0a06464b14d2dda6f853748e524a6ca1f39ced0b @@ -0,0 +1,2 @@ +go test fuzz v1 +string("class A implements Namespace{permits={A:(ctx)=>this.related.A.traverse(p=>p.permits") diff --git a/schema/typechecks.go b/schema/typechecks.go new file mode 100644 index 0000000..d2c2773 --- /dev/null +++ b/schema/typechecks.go @@ -0,0 +1,130 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package schema + +import "github.com/ory/keto/internal/namespace/ast" + +type ( + namespaceQuery []namespace + relationQuery []ast.Relation + typeCheck func(p *parser) +) + +func (p *parser) query() namespaceQuery { + return p.namespaces +} + +func (ns namespaceQuery) find(name string) (*namespace, bool) { + for _, n := range ns { + if n.Name == name { + return &n, true + } + } + return nil, false +} + +func (ns namespaceQuery) findRelation(namespace, relation string) (*ast.Relation, bool) { + n, ok := ns.find(namespace) + if !ok { + return nil, false + } + return relationQuery(n.Relations).find(relation) +} + +func (rs relationQuery) find(name string) (*ast.Relation, bool) { + for _, r := range rs { + if r.Name == name { + return &r, true + } + } + return nil, false +} + +func (p *parser) typeCheck() { + for _, check := range p.checks { + check(p) + } +} + +func (p *parser) addCheck(check typeCheck) { + p.checks = append(p.checks, check) +} + +// checkNamespace checks that the there exists a namespace with the given name. +func checkNamespaceExists(namespace item) typeCheck { + return func(p *parser) { + if _, ok := namespaceQuery(p.namespaces).find(namespace.Val); ok { + return + } + p.addErr(namespace, "namespace %q was not declared", namespace.Val) + } +} + +// checkNamespaceHasRelation checks that 1. there exists the given namespace, +// and 2. that there exists the given relation in that namespace. +func checkNamespaceHasRelation(namespace, relation item) typeCheck { + return func(p *parser) { + if n, ok := namespaceQuery(p.namespaces).find(namespace.Val); ok { + if _, ok := relationQuery(n.Relations).find(relation.Val); ok { + return + } + p.addErr(relation, + "namespace %q did not declare relation %q", + namespace.Val, relation.Val) + return + } + p.addErr(namespace, "namespace %q was not declared", namespace.Val) + } +} + +// checkCurrentNamespaceHasRelation checks that the give relation exists in the +// current namespace. +func checkCurrentNamespaceHasRelation(current *namespace, relation item) typeCheck { + namespace := current.Name + return func(p *parser) { + if n, ok := namespaceQuery(p.namespaces).find(namespace); ok { + if _, ok := relationQuery(n.Relations).find(relation.Val); ok { + return + } + p.addErr(relation, + "namespace %q did not declare relation %q", + namespace, relation.Val) + return + } + p.addErr(relation, "namespace %q was not declared", namespace) + } +} + +func checkAllRelationsTypesHaveRelation(current *namespace, relationType item, relation string) typeCheck { + namespace := current.Name + return func(p *parser) { + recursiveCheckAllRelationsTypesHaveRelation(p, relationType, namespace, relationType.Val, relation, tupleToSubjectSetTypeCheckMaxDepth) + } +} + +func recursiveCheckAllRelationsTypesHaveRelation(p *parser, item item, namespace string, relationType string, relation string, depth int) { + if depth < 0 { + p.addErr(item, "could not typecheck deeply nested SubjectSet further") + return + } + r, ok := namespaceQuery(p.namespaces).findRelation(namespace, relationType) + if !ok { + p.addErr(item, "relation %q was not declared in namespace %q", + relationType, namespace) + return + } + for _, t := range r.Types { + if t.Relation == "" { + if _, ok := p.query().findRelation(t.Namespace, relation); !ok { + p.addErr(item, "relation %q was not declared in namespace %q", + relation, t.Namespace) + } + } else { + // Type is a subject set, we need to recursively check if the type has + // the required relation. + recursiveCheckAllRelationsTypesHaveRelation( + p, item, t.Namespace, t.Relation, relation, depth-1) + } + } +} diff --git a/scripts/.gitattributes b/scripts/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/scripts/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/scripts/create-many-tuples.sh b/scripts/create-many-tuples.sh new file mode 100755 index 0000000..eee2d82 --- /dev/null +++ b/scripts/create-many-tuples.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +for i in {1..500} ; do + d="$(mktemp -d)" + for j in {1..20} ; do + echo "[" > "$d/$j.json" + for k in {1..100}; do + echo '{"namespace": "default", "object": "'$i'", "subject_id": "'$j'", "relation": "'$k'"},' >> "$d/$j.json" + done + echo '{"namespace": "default", "object": "'$i'", "subject_id": "'$j'", "relation": "last"}]' >> "$d/$j.json" + done + ./keto relation-tuple create -f "$d" +done diff --git a/scripts/install-grype.sh b/scripts/install-grype.sh new file mode 100755 index 0000000..4ad99f0 --- /dev/null +++ b/scripts/install-grype.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/install-helpers.sh + +BINDIR=${BINDIR:-./.bin} +GRYPE_VERSION="0.50.2" + +OS=$(uname_os) +BINNAME="grype" +if [ "${OS}" = "windows" ]; then + BINNAME="grype.exe" +fi + +if [[ "$("$BINDIR/$BINNAME" version)" == *"$GRYPE_VERSION"* ]]; then + echo "grype ${GRYPE_VERSION} already installed" + exit 0 +fi + +curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | bash -s -- -b "$BINDIR" "v$GRYPE_VERSION" diff --git a/scripts/install-helpers.sh b/scripts/install-helpers.sh new file mode 100755 index 0000000..fcf005f --- /dev/null +++ b/scripts/install-helpers.sh @@ -0,0 +1,93 @@ +#!/bin/env bash +set -euo pipefail + +is_command() { + command -v "$1" >/dev/null +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + darwin) os="osx" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + esac + echo ${arch} +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + echo "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl --retry 7 --retry-connrefused -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + echo "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + echo "downloading $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + echo "http_download unable to find wget or curl" + return 1 +} +update_binary_version() { + bindir=$1 + binname=$2 + version=$3 + echo "updating binary version to ${version}" + echo "${version}" > "${bindir}/.${binname}.version" +} +check_binary_version() { + bindir=$1 + binname=$2 + version=$3 + echo "checking binary version" + [ "$(cat "${bindir}/.${binname}.version")" = "${version}" ] +} diff --git a/scripts/install-licenses.sh b/scripts/install-licenses.sh new file mode 100755 index 0000000..4fb8bfd --- /dev/null +++ b/scripts/install-licenses.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/install-helpers.sh + +BINDIR=${BINDIR:-./.bin} +LICENCES_VERSION="0.1.48" + +OS=$(uname_os) +BINNAME="licences" +if [ "${OS}" = "windows" ]; then + BINNAME="licences.exe" +fi + +if [[ "$("$BINDIR/$BINNAME" version)" == *"$LICENCES_VERSION"* ]]; then + echo "licences ${LICENCES_VERSION} already installed" + exit 0 +fi + +curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/ory/ci/master/licenses/install | bash -s -- -b "$BINDIR" licences "v$LICENCES_VERSION" diff --git a/scripts/install-ory.sh b/scripts/install-ory.sh new file mode 100755 index 0000000..c28ac88 --- /dev/null +++ b/scripts/install-ory.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./scripts/install-helpers.sh + +BINDIR=${BINDIR:-./.bin} +ORY_VERSION="0.3.1" + +OS=$(uname_os) +BINNAME="ory" +if [ "${OS}" = "windows" ]; then + BINNAME="ory.exe" +fi + +if [[ "$("$BINDIR/$BINNAME" version)" == *"$ORY_VERSION"* ]]; then + echo "ory ${ORY_VERSION} already installed" + exit 0 +fi + +curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b "$BINDIR" ory "v$ORY_VERSION" diff --git a/scripts/render-schemas.sh b/scripts/render-schemas.sh new file mode 100755 index 0000000..bc5843d --- /dev/null +++ b/scripts/render-schemas.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +schema_version="${1:-$(git rev-parse --short HEAD)}" + +sed "s!ory://tracing-config!https://raw.githubusercontent.com/ory/keto/$schema_version/oryx/otelx/config.schema.json!g; +s!ory://logging-config!https://raw.githubusercontent.com/ory/keto/$schema_version/oryx/logrusx/config.schema.json!g" embedx/config.schema.json > .schema/config.schema.json + +git add .schema/config.schema.json + +if ! git diff --exit-code .schema/config.schema.json +then + git commit -m "autogen: render config schema" + git push +fi diff --git a/scripts/run-configuration.sh b/scripts/run-configuration.sh new file mode 100755 index 0000000..6721ec9 --- /dev/null +++ b/scripts/run-configuration.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -Eeuxo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +cat > configuration.md << EOF +--- +id: configuration +title: Configuration +--- + +\`\`\`yaml +`cat ./docs/config.yaml` +\`\`\` + +EOF diff --git a/scripts/run-deploy.sh b/scripts/run-deploy.sh new file mode 100755 index 0000000..2691f75 --- /dev/null +++ b/scripts/run-deploy.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -euo pipefail + +gox -ldflags "-X github.com/ory/keto/cmd.Version=`git describe --tags` -X github.com/ory/keto/cmd.BuildTime=`TZ=UTC date -u '+%Y-%m-%dT%H:%M:%SZ'` -X github.com/ory/keto/cmd.GitHash=`git rev-parse HEAD`" -output "dist/{{.Dir}}-{{.OS}}-{{.Arch}}"; +npm version -f --no-git-tag-version $(git describe --tag); diff --git a/scripts/single-table-migration-e2e.sh b/scripts/single-table-migration-e2e.sh new file mode 100755 index 0000000..2d347fd --- /dev/null +++ b/scripts/single-table-migration-e2e.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -euxo pipefail + +rm migrate_e2e.sqlite || true + +bash <(curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/keto/master/install.sh) -b . v0.6.0-alpha.3 + +export DSN="sqlite://./migrate_e2e.sqlite?_fk=true" +export KETO_READ_REMOTE="127.0.0.1:4466" +export KETO_WRITE_REMOTE="127.0.0.1:4467" + +config="$(mktemp --tmpdir keto.XXXXXX.yml)" +echo " +log: + level: debug + +namespaces: + - id: 0 + name: a + - id: 1 + name: b +" >> "$config" + +./keto migrate up -y -c "$config" --all-namespaces + +./keto serve all -c "$config" & +keto_server_pid=$! + +function teardown() { + kill $keto_server_pid || true +} +trap teardown EXIT + +jq '[range(300)] | map({namespace: (if . % 2 == 0 then "a" else "b" end), object: . | tostring, relation: "view", subject: "user"})' <(echo '{}') \ + | ./keto relation-tuple create -q -f - + +kill $keto_server_pid + +go build -o keto_new . + +./keto_new migrate up -y -c "$config" +./keto_new namespace migrate legacy -y -c "$config" + +./keto_new serve all -c "$config" & +keto_server_pid=$! + +for i in {0..300..24} ; do + diff <(echo 'Allowed') <(./keto_new check user view a "$i") +done + +for i in {1..300..24} ; do + diff <(echo 'Allowed') <(./keto_new check user view b "$i") +done diff --git a/scripts/test-resetdb.sh b/scripts/test-resetdb.sh new file mode 100755 index 0000000..5895cb1 --- /dev/null +++ b/scripts/test-resetdb.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euxo pipefail + +docker rm -f test_keto_postgres || true +docker rm -f test_keto_mysql || true +docker rm -f test_keto_cockroach || true + +postgres_port="$(docker port "$(docker run --name test_keto_postgres -e "POSTGRES_PASSWORD=secret" -e "POSTGRES_DB=postgres" -p 0.0.0.0:0:5432 -d postgres:16)" 5432 | sed 's/.*:\([0-9]*\)/\1/')" +mysql_port="$(docker port "$(docker run --name test_keto_mysql -e "MYSQL_ROOT_PASSWORD=secret" -p 0.0.0.0:0:3306 -d mysql:8.0)" 3306 | sed 's/.*:\([0-9]*\)/\1/')" +cockroach_port="$(docker port "$(docker run --name test_keto_cockroach -p 0.0.0.0:0:26257 -d cockroachdb/cockroach:latest-v25.4 start-single-node --insecure)" 26257 | sed 's/.*:\([0-9]*\)/\1/')" + +TEST_DATABASE_POSTGRESQL=$(printf "postgres://postgres:secret@localhost:%s/postgres?sslmode=disable" "$postgres_port") +TEST_DATABASE_MYSQL=$(printf "mysql://root:secret@(localhost:%s)/mysql?parseTime=true&multiStatements=true" "$mysql_port") +TEST_DATABASE_COCKROACHDB=$(printf "cockroach://root@localhost:%s/defaultdb?sslmode=disable" "$cockroach_port") + +export TEST_DATABASE_POSTGRESQL +export TEST_DATABASE_MYSQL +export TEST_DATABASE_COCKROACHDB + +# undo set from above +set +e +set +u +set +x +set +o pipefail diff --git a/scripts/test-sdk.sh b/scripts/test-sdk.sh new file mode 100755 index 0000000..4bf29e6 --- /dev/null +++ b/scripts/test-sdk.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -euo pipefail + +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +scripts/run-genswag.sh +git add -A +git diff --exit-code + +./scripts/run-gensdk.sh +git add -A +git diff --exit-code diff --git a/spec/api.json b/spec/api.json new file mode 100755 index 0000000..ac30fd3 --- /dev/null +++ b/spec/api.json @@ -0,0 +1,1467 @@ +{ + "components": { + "responses": { + "emptyResponse": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." + } + }, + "schemas": { + "DefaultError": { + "description": "From https://go.dev/wiki/CodeReviewComments#receiver-type:\n\u003e Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer.\n\u003e If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating,\n\u003e prefer a pointer receiver, as it will make the intention clearer to the reader.\n\u003e Don’t mix receiver types. Choose either pointers or struct types for all available methods.", + "title": "DefaultError is not safe to shallow copy because it contains\nfields which are not value types, e.g. maps.\nA shallow copy would inadvertently share these underlying fields,\nleading to incorrect behavior and data races in a concurrent context.\nConsequently, all methods take the type by pointer and not by value." + }, + "ParseError": { + "properties": { + "end": { + "$ref": "#/components/schemas/SourcePosition" + }, + "message": { + "type": "string" + }, + "start": { + "$ref": "#/components/schemas/SourcePosition" + } + }, + "type": "object" + }, + "SourcePosition": { + "properties": { + "Line": { + "format": "int64", + "type": "integer" + }, + "column": { + "format": "int64", + "type": "integer" + } + }, + "type": "object" + }, + "UUID": { + "format": "uuid4", + "type": "string" + }, + "batchCheckPermissionBody": { + "description": "Batch Check Permission Body", + "properties": { + "tuples": { + "items": { + "$ref": "#/components/schemas/relationship" + }, + "type": "array" + } + }, + "type": "object" + }, + "batchCheckPermissionResult": { + "description": "Batch Check Permission Result", + "properties": { + "results": { + "description": "An array of check results. The order aligns with the input order.", + "items": { + "$ref": "#/components/schemas/checkPermissionResultWithError" + }, + "type": "array" + } + }, + "required": ["results"], + "type": "object" + }, + "checkOplSyntaxBody": { + "description": "Ory Permission Language Document", + "type": "string" + }, + "checkOplSyntaxResult": { + "properties": { + "errors": { + "description": "The list of syntax errors", + "items": { + "$ref": "#/components/schemas/ParseError" + }, + "type": "array" + } + }, + "title": "CheckOPLSyntaxResponse represents the response for an OPL syntax check request.", + "type": "object" + }, + "checkPermissionResult": { + "description": "The content of the allowed field is mirrored in the HTTP status code.", + "properties": { + "allowed": { + "description": "whether the relation tuple is allowed", + "type": "boolean" + } + }, + "required": ["allowed"], + "title": "Check Permission Result", + "type": "object" + }, + "checkPermissionResultWithError": { + "description": "Check Permission Result With Error", + "properties": { + "allowed": { + "description": "whether the relation tuple is allowed", + "type": "boolean" + }, + "error": { + "description": "any error generated while checking the relation tuple", + "type": "string" + } + }, + "required": ["allowed"], + "type": "object" + }, + "createRelationshipBody": { + "description": "Create Relationship Request Body", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/components/schemas/subjectSet" + } + }, + "type": "object" + }, + "errorGeneric": { + "description": "The standard Ory JSON API error format.", + "properties": { + "error": { + "$ref": "#/components/schemas/genericError" + } + }, + "required": ["error"], + "title": "JSON API Error Response", + "type": "object" + }, + "expandedPermissionTree": { + "properties": { + "children": { + "description": "The children of the node, possibly none.", + "items": { + "$ref": "#/components/schemas/expandedPermissionTree" + }, + "type": "array" + }, + "tuple": { + "$ref": "#/components/schemas/relationship" + }, + "type": { + "description": "The type of the node.\nunion TreeNodeUnion\nexclusion TreeNodeExclusion\nintersection TreeNodeIntersection\nleaf TreeNodeLeaf\ntuple_to_subject_set TreeNodeTupleToSubjectSet\ncomputed_subject_set TreeNodeComputedSubjectSet\nnot TreeNodeNot\nunspecified TreeNodeUnspecified", + "enum": [ + "union", + "exclusion", + "intersection", + "leaf", + "tuple_to_subject_set", + "computed_subject_set", + "not", + "unspecified" + ], + "type": "string", + "x-go-enum-desc": "union TreeNodeUnion\nexclusion TreeNodeExclusion\nintersection TreeNodeIntersection\nleaf TreeNodeLeaf\ntuple_to_subject_set TreeNodeTupleToSubjectSet\ncomputed_subject_set TreeNodeComputedSubjectSet\nnot TreeNodeNot\nunspecified TreeNodeUnspecified" + } + }, + "required": ["type"], + "type": "object" + }, + "genericError": { + "properties": { + "code": { + "description": "The status code", + "example": 404, + "format": "int64", + "type": "integer" + }, + "debug": { + "description": "Debug information\n\nThis field is often not exposed to protect against leaking\nsensitive information.", + "example": "SQL field \"foo\" is not a bool.", + "type": "string" + }, + "details": { + "additionalProperties": {}, + "description": "Further error details", + "type": "object" + }, + "id": { + "description": "The error ID\n\nUseful when trying to identify various errors in application logic.", + "type": "string" + }, + "message": { + "description": "Error message\n\nThe error's message.", + "example": "The resource could not be found", + "type": "string" + }, + "reason": { + "description": "A human-readable reason for the error", + "example": "User with ID 1234 does not exist.", + "type": "string" + }, + "request": { + "description": "The request ID\n\nThe request ID is often exposed internally in order to trace\nerrors across service architectures. This is often a UUID.", + "example": "d7ef54b1-ec15-46e6-bccb-524b82c035e6", + "type": "string" + }, + "status": { + "description": "The status description", + "example": "Not Found", + "type": "string" + } + }, + "required": ["message"], + "type": "object" + }, + "healthNotReadyStatus": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "title": "The not ready status of the service.", + "type": "object" + }, + "healthStatus": { + "properties": { + "status": { + "description": "Status always contains \"ok\".", + "type": "string" + } + }, + "title": "The health status of the service.", + "type": "object" + }, + "namespace": { + "properties": { + "name": { + "description": "Name of the namespace.", + "type": "string" + } + }, + "type": "object" + }, + "postCheckPermissionBody": { + "description": "Check Permission using Post Request Body", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/components/schemas/subjectSet" + } + }, + "type": "object" + }, + "postCheckPermissionOrErrorBody": { + "description": "Post Check Permission Or Error Body", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/components/schemas/subjectSet" + } + }, + "type": "object" + }, + "relationQuery": { + "description": "Relation Query", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/components/schemas/subjectSet" + } + }, + "type": "object" + }, + "relationship": { + "description": "Relationship", + "properties": { + "namespace": { + "description": "Namespace of the Relation Tuple", + "type": "string" + }, + "object": { + "description": "Object of the Relation Tuple", + "type": "string" + }, + "relation": { + "description": "Relation of the Relation Tuple", + "type": "string" + }, + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/components/schemas/subjectSet" + } + }, + "required": ["namespace", "object", "relation"], + "type": "object" + }, + "relationshipNamespaces": { + "description": "Relationship Namespace List", + "properties": { + "namespaces": { + "items": { + "$ref": "#/components/schemas/namespace" + }, + "type": "array" + } + }, + "type": "object" + }, + "relationshipPatch": { + "description": "Payload for patching a relationship", + "properties": { + "action": { + "enum": ["insert", "delete"], + "type": "string", + "x-go-enum-desc": "insert ActionInsert\ndelete ActionDelete" + }, + "relation_tuple": { + "$ref": "#/components/schemas/relationship" + } + }, + "type": "object" + }, + "relationships": { + "description": "Paginated Relationship List", + "properties": { + "next_page_token": { + "description": "The opaque token to provide in a subsequent request\nto get the next page. It is the empty string iff this is\nthe last page.", + "type": "string" + }, + "relation_tuples": { + "items": { + "$ref": "#/components/schemas/relationship" + }, + "type": "array" + } + }, + "type": "object" + }, + "subjectSet": { + "properties": { + "namespace": { + "description": "Namespace of the Subject Set", + "type": "string" + }, + "object": { + "description": "Object of the Subject Set", + "type": "string" + }, + "relation": { + "description": "Relation of the Subject Set", + "type": "string" + } + }, + "required": ["namespace", "object", "relation"], + "type": "object" + }, + "unexpectedError": { + "type": "string" + }, + "version": { + "properties": { + "version": { + "description": "Version is the service's version.", + "type": "string" + } + }, + "type": "object" + } + } + }, + "info": { + "contact": { + "email": "hi@ory.sh" + }, + "description": "Documentation for all of Ory Keto's REST APIs. gRPC is documented separately.\n", + "license": { + "name": "Apache 2.0" + }, + "title": "Ory Keto API", + "version": "" + }, + "openapi": "3.0.3", + "paths": { + "/admin/relation-tuples": { + "delete": { + "description": "Use this endpoint to delete relationships", + "operationId": "deleteRelationships", + "parameters": [ + { + "description": "Namespace of the Relationship", + "in": "query", + "name": "namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Relationship", + "in": "query", + "name": "object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Relationship", + "in": "query", + "name": "relation", + "schema": { + "type": "string" + } + }, + { + "description": "SubjectID of the Relationship", + "in": "query", + "name": "subject_id", + "schema": { + "type": "string" + } + }, + { + "description": "Namespace of the Subject Set", + "in": "query", + "name": "subject_set.namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Subject Set", + "in": "query", + "name": "subject_set.object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Subject Set", + "in": "query", + "name": "subject_set.relation", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Delete Relationships", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-low" + }, + "patch": { + "description": "Use this endpoint to patch one or more relationships.", + "operationId": "patchRelationships", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/relationshipPatch" + }, + "type": "array" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "204": { + "$ref": "#/components/responses/emptyResponse" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Patch Multiple Relationships", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-low" + }, + "put": { + "description": "Use this endpoint to create a relationship.", + "operationId": "createRelationship", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/createRelationshipBody" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/relationship" + } + } + }, + "description": "relationship" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Create a Relationship", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-low" + } + }, + "/health/alive": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Keto is accepting incoming\nHTTP requests. This status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isAlive", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Always \"ok\".", + "type": "string" + } + }, + "required": ["status"], + "type": "object" + } + } + }, + "description": "Ory Keto is ready to accept connections." + }, + "default": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "Unexpected error" + } + }, + "summary": "Check HTTP Server Status", + "tags": ["metadata"] + } + }, + "/health/ready": { + "get": { + "description": "This endpoint returns a HTTP 200 status code when Ory Keto is up running and the environment dependencies (e.g.\nthe database) are responsive as well.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of Ory Keto, the health status will never\nrefer to the cluster state, only to a single instance.", + "operationId": "isReady", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "status": { + "description": "Always \"ok\".", + "type": "string" + } + }, + "required": ["status"], + "type": "object" + } + } + }, + "description": "Ory Keto is ready to accept requests." + }, + "503": { + "content": { + "application/json": { + "schema": { + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object" + } + }, + "required": ["errors"], + "type": "object" + } + } + }, + "description": "Ory Kratos is not yet ready to accept requests." + }, + "default": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + }, + "description": "Unexpected error" + } + }, + "summary": "Check HTTP Server and Database Status", + "tags": ["metadata"] + } + }, + "/namespaces": { + "get": { + "description": "Get all namespaces", + "operationId": "listRelationshipNamespaces", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/relationshipNamespaces" + } + } + }, + "description": "relationshipNamespaces" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Query namespaces", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-low" + } + }, + "/opl/syntax/check": { + "post": { + "description": "The OPL file is expected in the body of the request.", + "operationId": "checkOplSyntax", + "requestBody": { + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/checkOplSyntaxBody" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkOplSyntaxResult" + } + } + }, + "description": "checkOplSyntaxResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Check the syntax of an OPL file", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/relation-tuples": { + "get": { + "description": "Get all relationships that match the query. Only the namespace field is required.", + "operationId": "getRelationships", + "parameters": [ + { + "description": "Items per Page\n\nThis is the number of items per page to return.\nFor details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).", + "in": "query", + "name": "page_size", + "schema": { + "default": 250, + "format": "int64", + "maximum": 1000, + "minimum": 1, + "type": "integer" + } + }, + { + "description": "Next Page Token\n\nThe next page token.\nFor details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).", + "in": "query", + "name": "page_token", + "schema": { + "type": "string" + } + }, + { + "description": "Namespace of the Relationship", + "in": "query", + "name": "namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Relationship", + "in": "query", + "name": "object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Relationship", + "in": "query", + "name": "relation", + "schema": { + "type": "string" + } + }, + { + "description": "SubjectID of the Relationship", + "in": "query", + "name": "subject_id", + "schema": { + "type": "string" + } + }, + { + "description": "Namespace of the Subject Set", + "in": "query", + "name": "subject_set.namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Subject Set", + "in": "query", + "name": "subject_set.object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Subject Set", + "in": "query", + "name": "subject_set.relation", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/relationships" + } + } + }, + "description": "relationships" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Query relationships", + "tags": ["relationship"], + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/relation-tuples/batch/check": { + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "operationId": "batchCheckPermission", + "parameters": [ + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batchCheckPermissionBody" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batchCheckPermissionResult" + } + } + }, + "description": "batchCheckPermissionResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Batch check permissions", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/check": { + "get": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "operationId": "checkPermissionOrError", + "parameters": [ + { + "description": "Namespace of the Relationship", + "in": "query", + "name": "namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Relationship", + "in": "query", + "name": "object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Relationship", + "in": "query", + "name": "relation", + "schema": { + "type": "string" + } + }, + { + "description": "SubjectID of the Relationship", + "in": "query", + "name": "subject_id", + "schema": { + "type": "string" + } + }, + { + "description": "Namespace of the Subject Set", + "in": "query", + "name": "subject_set.namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Subject Set", + "in": "query", + "name": "subject_set.object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Subject Set", + "in": "query", + "name": "subject_set.relation", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Check a permission", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-public-high" + }, + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "operationId": "postCheckPermissionOrError", + "parameters": [ + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/postCheckPermissionOrErrorBody" + } + } + }, + "x-originalParamName": "Body" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Check a permission", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/check/openapi": { + "get": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "operationId": "checkPermission", + "parameters": [ + { + "description": "Namespace of the Relationship", + "in": "query", + "name": "namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Relationship", + "in": "query", + "name": "object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Relationship", + "in": "query", + "name": "relation", + "schema": { + "type": "string" + } + }, + { + "description": "SubjectID of the Relationship", + "in": "query", + "name": "subject_id", + "schema": { + "type": "string" + } + }, + { + "description": "Namespace of the Subject Set", + "in": "query", + "name": "subject_set.namespace", + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Subject Set", + "in": "query", + "name": "subject_set.object", + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Subject Set", + "in": "query", + "name": "subject_set.relation", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Check a permission", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-public-high" + }, + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "operationId": "postCheckPermission", + "parameters": [ + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/postCheckPermissionBody" + } + } + }, + "x-originalParamName": "Payload" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/checkPermissionResult" + } + } + }, + "description": "checkPermissionResult" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Check a permission", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/expand": { + "get": { + "description": "Use this endpoint to expand a relationship tuple into permissions.", + "operationId": "expandPermissions", + "parameters": [ + { + "description": "Namespace of the Subject Set", + "in": "query", + "name": "namespace", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Object of the Subject Set", + "in": "query", + "name": "object", + "required": true, + "schema": { + "type": "string" + } + }, + { + "description": "Relation of the Subject Set", + "in": "query", + "name": "relation", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "max-depth", + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/expandedPermissionTree" + } + } + }, + "description": "expandedPermissionTree" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorGeneric" + } + } + }, + "description": "errorGeneric" + } + }, + "summary": "Expand a Relationship into permissions.", + "tags": ["permission"], + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/version": { + "get": { + "description": "This endpoint returns the version of Ory Keto.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the version will never\nrefer to the cluster state, only to a single instance.", + "operationId": "getVersion", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "version": { + "description": "The version of Ory Keto.", + "type": "string" + } + }, + "required": ["version"], + "type": "object" + } + } + }, + "description": "Returns the Ory Keto version." + } + }, + "summary": "Return Running Software Version.", + "tags": ["metadata"] + } + } + } +} diff --git a/spec/files.go b/spec/files.go new file mode 100644 index 0000000..3e8dbef --- /dev/null +++ b/spec/files.go @@ -0,0 +1,9 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package spec + +import _ "embed" + +//go:embed api.json +var API []byte diff --git a/spec/swagger.json b/spec/swagger.json new file mode 100755 index 0000000..57afc1e --- /dev/null +++ b/spec/swagger.json @@ -0,0 +1,1214 @@ +{ + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "swagger": "2.0", + "info": { + "description": "Ory Keto is a cloud native access control server providing best-practice patterns (RBAC, ABAC, ACL, AWS IAM Policies, Kubernetes Roles, ...) via REST APIs.", + "title": "ORY Keto", + "contact": { + "name": "ORY", + "url": "https://www.ory.com", + "email": "hi@ory.com" + }, + "license": { + "name": "Apache 2.0", + "url": "https://github.com/ory/keto/blob/master/LICENSE" + }, + "version": "Latest" + }, + "basePath": "/", + "paths": { + "/admin/relation-tuples": { + "put": { + "description": "Use this endpoint to create a relationship.", + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Create a Relationship", + "operationId": "createRelationship", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/createRelationshipBody" + } + } + ], + "responses": { + "201": { + "description": "relationship", + "schema": { + "$ref": "#/definitions/relationship" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-low" + }, + "delete": { + "description": "Use this endpoint to delete relationships", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Delete Relationships", + "operationId": "deleteRelationships", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relationship", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Relationship", + "name": "object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Relationship", + "name": "relation", + "in": "query" + }, + { + "type": "string", + "description": "SubjectID of the Relationship", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + } + ], + "responses": { + "204": { + "$ref": "#/responses/emptyResponse" + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-low" + }, + "patch": { + "description": "Use this endpoint to patch one or more relationships.", + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Patch Multiple Relationships", + "operationId": "patchRelationships", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/relationshipPatch" + } + } + } + ], + "responses": { + "204": { + "$ref": "#/responses/emptyResponse" + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "404": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-low" + } + }, + "/health/alive": { + "get": { + "description": "This endpoint returns a 200 status code when the HTTP server is up running.\nThis status does currently not include checks whether the database connection is working.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": ["application/json", "text/plain"], + "tags": ["health"], + "summary": "Check alive status", + "operationId": "isInstanceAlive", + "responses": { + "200": { + "description": "healthStatus", + "schema": { + "$ref": "#/definitions/healthStatus" + } + }, + "default": { + "description": "unexpectedError", + "schema": { + "$ref": "#/definitions/unexpectedError" + } + } + } + } + }, + "/health/ready": { + "get": { + "description": "This endpoint returns a 200 status code when the HTTP server is up running and the environment dependencies (e.g.\nthe database) are responsive as well.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": ["application/json", "text/plain"], + "tags": ["health"], + "summary": "Check readiness status", + "operationId": "isInstanceReady", + "responses": { + "200": { + "description": "healthStatus", + "schema": { + "$ref": "#/definitions/healthStatus" + } + }, + "503": { + "description": "healthNotReadyStatus", + "schema": { + "$ref": "#/definitions/healthNotReadyStatus" + } + }, + "default": { + "description": "unexpectedError", + "schema": { + "$ref": "#/definitions/unexpectedError" + } + } + } + } + }, + "/namespaces": { + "get": { + "description": "Get all namespaces", + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Query namespaces", + "operationId": "listRelationshipNamespaces", + "responses": { + "200": { + "description": "relationshipNamespaces", + "schema": { + "$ref": "#/definitions/relationshipNamespaces" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-low" + } + }, + "/opl/syntax/check": { + "post": { + "description": "The OPL file is expected in the body of the request.", + "consumes": ["text/plain"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Check the syntax of an OPL file", + "operationId": "checkOplSyntax", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/checkOplSyntaxBody" + } + } + ], + "responses": { + "200": { + "description": "checkOplSyntaxResult", + "schema": { + "$ref": "#/definitions/checkOplSyntaxResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/relation-tuples": { + "get": { + "description": "Get all relationships that match the query. Only the namespace field is required.", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["relationship"], + "summary": "Query relationships", + "operationId": "getRelationships", + "parameters": [ + { + "maximum": 1000, + "minimum": 1, + "type": "integer", + "format": "int64", + "default": 250, + "description": "Items per Page\n\nThis is the number of items per page to return.\nFor details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).", + "name": "page_size", + "in": "query" + }, + { + "type": "string", + "description": "Next Page Token\n\nThe next page token.\nFor details on pagination please head over to the [pagination documentation](https://www.ory.com/docs/ecosystem/api-design#pagination).", + "name": "page_token", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Relationship", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Relationship", + "name": "object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Relationship", + "name": "relation", + "in": "query" + }, + { + "type": "string", + "description": "SubjectID of the Relationship", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + } + ], + "responses": { + "200": { + "description": "relationships", + "schema": { + "$ref": "#/definitions/relationships" + } + }, + "404": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/relation-tuples/batch/check": { + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Batch check permissions", + "operationId": "batchCheckPermission", + "parameters": [ + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/batchCheckPermissionBody" + } + } + ], + "responses": { + "200": { + "description": "batchCheckPermissionResult", + "schema": { + "$ref": "#/definitions/batchCheckPermissionResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/check": { + "get": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Check a permission", + "operationId": "checkPermissionOrError", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relationship", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Relationship", + "name": "object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Relationship", + "name": "relation", + "in": "query" + }, + { + "type": "string", + "description": "SubjectID of the Relationship", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + } + ], + "responses": { + "200": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "403": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-public-high" + }, + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Check a permission", + "operationId": "postCheckPermissionOrError", + "parameters": [ + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/postCheckPermissionOrErrorBody" + } + } + ], + "responses": { + "200": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "403": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/check/openapi": { + "get": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Check a permission", + "operationId": "checkPermission", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Relationship", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Relationship", + "name": "object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Relationship", + "name": "relation", + "in": "query" + }, + { + "type": "string", + "description": "SubjectID of the Relationship", + "name": "subject_id", + "in": "query" + }, + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "subject_set.namespace", + "in": "query" + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "subject_set.object", + "in": "query" + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "subject_set.relation", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + } + ], + "responses": { + "200": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-public-high" + }, + "post": { + "description": "To learn how relationship tuples and the check works, head over to [the documentation](https://www.ory.com/docs/keto/concepts/api-overview).", + "consumes": ["application/json"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Check a permission", + "operationId": "postCheckPermission", + "parameters": [ + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + }, + { + "name": "Payload", + "in": "body", + "schema": { + "$ref": "#/definitions/postCheckPermissionBody" + } + } + ], + "responses": { + "200": { + "description": "checkPermissionResult", + "schema": { + "$ref": "#/definitions/checkPermissionResult" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-public-high" + } + }, + "/relation-tuples/expand": { + "get": { + "description": "Use this endpoint to expand a relationship tuple into permissions.", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "schemes": ["http", "https"], + "tags": ["permission"], + "summary": "Expand a Relationship into permissions.", + "operationId": "expandPermissions", + "parameters": [ + { + "type": "string", + "description": "Namespace of the Subject Set", + "name": "namespace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Object of the Subject Set", + "name": "object", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Relation of the Subject Set", + "name": "relation", + "in": "query", + "required": true + }, + { + "type": "integer", + "format": "int64", + "name": "max-depth", + "in": "query" + } + ], + "responses": { + "200": { + "description": "expandedPermissionTree", + "schema": { + "$ref": "#/definitions/expandedPermissionTree" + } + }, + "400": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "404": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + }, + "default": { + "description": "errorGeneric", + "schema": { + "$ref": "#/definitions/errorGeneric" + } + } + }, + "x-ory-ratelimit-bucket": "keto-admin-medium" + } + }, + "/version": { + "get": { + "description": "This endpoint returns the service version typically notated using semantic versioning.\n\nIf the service supports TLS Edge Termination, this endpoint does not require the\n`X-Forwarded-Proto` header to be set.\n\nBe aware that if you are running multiple nodes of this service, the health status will never\nrefer to the cluster state, only to a single instance.", + "produces": ["application/json"], + "tags": ["version"], + "summary": "Get service version", + "operationId": "getVersion", + "responses": { + "200": { + "description": "version", + "schema": { + "$ref": "#/definitions/version" + } + } + } + } + } + }, + "definitions": { + "DefaultError": { + "description": "From https://go.dev/wiki/CodeReviewComments#receiver-type:\n\u003e Can function or methods, either concurrently or when called from this method, be mutating the receiver? A value type creates a copy of the receiver when the method is invoked, so outside updates will not be applied to this receiver. If changes must be visible in the original receiver, the receiver must be a pointer.\n\u003e If the receiver is a struct, array or slice and any of its elements is a pointer to something that might be mutating,\n\u003e prefer a pointer receiver, as it will make the intention clearer to the reader.\n\u003e Don’t mix receiver types. Choose either pointers or struct types for all available methods.", + "title": "DefaultError is not safe to shallow copy because it contains\nfields which are not value types, e.g. maps.\nA shallow copy would inadvertently share these underlying fields,\nleading to incorrect behavior and data races in a concurrent context.\nConsequently, all methods take the type by pointer and not by value." + }, + "ParseError": { + "type": "object", + "properties": { + "end": { + "$ref": "#/definitions/SourcePosition" + }, + "message": { + "type": "string" + }, + "start": { + "$ref": "#/definitions/SourcePosition" + } + } + }, + "SourcePosition": { + "type": "object", + "properties": { + "Line": { + "type": "integer", + "format": "int64" + }, + "column": { + "type": "integer", + "format": "int64" + } + } + }, + "batchCheckPermissionBody": { + "description": "Batch Check Permission Body", + "type": "object", + "properties": { + "tuples": { + "type": "array", + "items": { + "$ref": "#/definitions/relationship" + } + } + } + }, + "batchCheckPermissionResult": { + "description": "Batch Check Permission Result", + "type": "object", + "required": ["results"], + "properties": { + "results": { + "description": "An array of check results. The order aligns with the input order.", + "type": "array", + "items": { + "$ref": "#/definitions/checkPermissionResultWithError" + } + } + } + }, + "checkOplSyntaxBody": { + "description": "Ory Permission Language Document", + "type": "string" + }, + "checkOplSyntaxResult": { + "type": "object", + "title": "CheckOPLSyntaxResponse represents the response for an OPL syntax check request.", + "properties": { + "errors": { + "description": "The list of syntax errors", + "type": "array", + "items": { + "$ref": "#/definitions/ParseError" + } + } + } + }, + "checkPermissionResult": { + "description": "The content of the allowed field is mirrored in the HTTP status code.", + "type": "object", + "title": "Check Permission Result", + "required": ["allowed"], + "properties": { + "allowed": { + "description": "whether the relation tuple is allowed", + "type": "boolean" + } + } + }, + "checkPermissionResultWithError": { + "description": "Check Permission Result With Error", + "type": "object", + "required": ["allowed"], + "properties": { + "allowed": { + "description": "whether the relation tuple is allowed", + "type": "boolean" + }, + "error": { + "description": "any error generated while checking the relation tuple", + "type": "string" + } + } + }, + "createRelationshipBody": { + "description": "Create Relationship Request Body", + "type": "object", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/subjectSet" + } + } + }, + "errorGeneric": { + "description": "The standard Ory JSON API error format.", + "type": "object", + "title": "JSON API Error Response", + "required": ["error"], + "properties": { + "error": { + "$ref": "#/definitions/genericError" + } + } + }, + "expandedPermissionTree": { + "type": "object", + "required": ["type"], + "properties": { + "children": { + "description": "The children of the node, possibly none.", + "type": "array", + "items": { + "$ref": "#/definitions/expandedPermissionTree" + } + }, + "tuple": { + "$ref": "#/definitions/relationship" + }, + "type": { + "description": "The type of the node.\nunion TreeNodeUnion\nexclusion TreeNodeExclusion\nintersection TreeNodeIntersection\nleaf TreeNodeLeaf\ntuple_to_subject_set TreeNodeTupleToSubjectSet\ncomputed_subject_set TreeNodeComputedSubjectSet\nnot TreeNodeNot\nunspecified TreeNodeUnspecified", + "type": "string", + "enum": [ + "union", + "exclusion", + "intersection", + "leaf", + "tuple_to_subject_set", + "computed_subject_set", + "not", + "unspecified" + ], + "x-go-enum-desc": "union TreeNodeUnion\nexclusion TreeNodeExclusion\nintersection TreeNodeIntersection\nleaf TreeNodeLeaf\ntuple_to_subject_set TreeNodeTupleToSubjectSet\ncomputed_subject_set TreeNodeComputedSubjectSet\nnot TreeNodeNot\nunspecified TreeNodeUnspecified" + } + } + }, + "genericError": { + "type": "object", + "required": ["message"], + "properties": { + "code": { + "description": "The status code", + "type": "integer", + "format": "int64", + "example": 404 + }, + "debug": { + "description": "Debug information\n\nThis field is often not exposed to protect against leaking\nsensitive information.", + "type": "string", + "example": "SQL field \"foo\" is not a bool." + }, + "details": { + "description": "Further error details", + "type": "object", + "additionalProperties": {} + }, + "id": { + "description": "The error ID\n\nUseful when trying to identify various errors in application logic.", + "type": "string" + }, + "message": { + "description": "Error message\n\nThe error's message.", + "type": "string", + "example": "The resource could not be found" + }, + "reason": { + "description": "A human-readable reason for the error", + "type": "string", + "example": "User with ID 1234 does not exist." + }, + "request": { + "description": "The request ID\n\nThe request ID is often exposed internally in order to trace\nerrors across service architectures. This is often a UUID.", + "type": "string", + "example": "d7ef54b1-ec15-46e6-bccb-524b82c035e6" + }, + "status": { + "description": "The status description", + "type": "string", + "example": "Not Found" + } + } + }, + "healthNotReadyStatus": { + "type": "object", + "title": "The not ready status of the service.", + "properties": { + "errors": { + "description": "Errors contains a list of errors that caused the not ready status.", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "healthStatus": { + "type": "object", + "title": "The health status of the service.", + "properties": { + "status": { + "description": "Status always contains \"ok\".", + "type": "string" + } + } + }, + "namespace": { + "type": "object", + "properties": { + "name": { + "description": "Name of the namespace.", + "type": "string" + } + } + }, + "postCheckPermissionBody": { + "description": "Check Permission using Post Request Body", + "type": "object", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/subjectSet" + } + } + }, + "postCheckPermissionOrErrorBody": { + "description": "Post Check Permission Or Error Body", + "type": "object", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/subjectSet" + } + } + }, + "relationQuery": { + "description": "Relation Query", + "type": "object", + "properties": { + "namespace": { + "description": "Namespace to query", + "type": "string" + }, + "object": { + "description": "Object to query", + "type": "string" + }, + "relation": { + "description": "Relation to query", + "type": "string" + }, + "subject_id": { + "description": "SubjectID to query\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/subjectSet" + } + } + }, + "relationship": { + "description": "Relationship", + "type": "object", + "required": ["namespace", "object", "relation"], + "properties": { + "namespace": { + "description": "Namespace of the Relation Tuple", + "type": "string" + }, + "object": { + "description": "Object of the Relation Tuple", + "type": "string" + }, + "relation": { + "description": "Relation of the Relation Tuple", + "type": "string" + }, + "subject_id": { + "description": "SubjectID of the Relation Tuple\n\nEither SubjectSet or SubjectID can be provided.", + "type": "string" + }, + "subject_set": { + "$ref": "#/definitions/subjectSet" + } + } + }, + "relationshipNamespaces": { + "description": "Relationship Namespace List", + "type": "object", + "properties": { + "namespaces": { + "type": "array", + "items": { + "$ref": "#/definitions/namespace" + } + } + } + }, + "relationshipPatch": { + "description": "Payload for patching a relationship", + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["insert", "delete"], + "x-go-enum-desc": "insert ActionInsert\ndelete ActionDelete" + }, + "relation_tuple": { + "$ref": "#/definitions/relationship" + } + } + }, + "relationships": { + "description": "Paginated Relationship List", + "type": "object", + "properties": { + "next_page_token": { + "description": "The opaque token to provide in a subsequent request\nto get the next page. It is the empty string iff this is\nthe last page.", + "type": "string" + }, + "relation_tuples": { + "type": "array", + "items": { + "$ref": "#/definitions/relationship" + } + } + } + }, + "subjectSet": { + "type": "object", + "required": ["namespace", "object", "relation"], + "properties": { + "namespace": { + "description": "Namespace of the Subject Set", + "type": "string" + }, + "object": { + "description": "Object of the Subject Set", + "type": "string" + }, + "relation": { + "description": "Relation of the Subject Set", + "type": "string" + } + } + }, + "unexpectedError": { + "type": "string" + }, + "version": { + "type": "object", + "properties": { + "version": { + "description": "Version is the service's version.", + "type": "string" + } + } + }, + "UUID": { "type": "string", "format": "uuid4" } + }, + "responses": { + "emptyResponse": { + "description": "Empty responses are sent when, for example, resources are deleted. The HTTP status code for empty responses is typically 204." + } + } +} diff --git a/x/events/events.go b/x/events/events.go new file mode 100644 index 0000000..7c532b2 --- /dev/null +++ b/x/events/events.go @@ -0,0 +1,55 @@ +// Copyright © 2023 Ory Corp +// SPDX-License-Identifier: Apache-2.0 + +package events + +import ( + "context" + + "github.com/ory/x/otelx/semconv" + "go.opentelemetry.io/otel/trace" +) + +const ( + RelationtuplesCreated semconv.Event = "RelationtuplesCreated" + RelationtuplesDeleted semconv.Event = "RelationtuplesDeleted" + RelationtuplesChanged semconv.Event = "RelationtuplesChanged" + + PermissionsExpanded semconv.Event = "PermissionsExpanded" + PermissionsChecked semconv.Event = "PermissionsChecked" +) + +func NewRelationtuplesCreated(ctx context.Context) (string, trace.EventOption) { + return RelationtuplesCreated.String(), + trace.WithAttributes( + semconv.AttributesFromContext(ctx)..., + ) +} + +func NewRelationtuplesDeleted(ctx context.Context) (string, trace.EventOption) { + return RelationtuplesDeleted.String(), + trace.WithAttributes( + semconv.AttributesFromContext(ctx)..., + ) +} + +func NewRelationtuplesChanged(ctx context.Context) (string, trace.EventOption) { + return RelationtuplesChanged.String(), + trace.WithAttributes( + semconv.AttributesFromContext(ctx)..., + ) +} + +func NewPermissionsExpanded(ctx context.Context) (string, trace.EventOption) { + return PermissionsExpanded.String(), + trace.WithAttributes( + semconv.AttributesFromContext(ctx)..., + ) +} + +func NewPermissionsChecked(ctx context.Context) (string, trace.EventOption) { + return PermissionsChecked.String(), + trace.WithAttributes( + semconv.AttributesFromContext(ctx)..., + ) +}