snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

22
.docker/Dockerfile Normal file
View File

@@ -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"]

14
.docker/Dockerfile-alpine Normal file
View File

@@ -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"]

38
.docker/Dockerfile-build Normal file
View File

@@ -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"]

View File

@@ -0,0 +1,7 @@
FROM gcr.io/distroless/static-debian12:nonroot
COPY keto /usr/bin/keto
EXPOSE 4466 4467
ENTRYPOINT ["keto"]
CMD ["serve"]

37
.docker/Dockerfile-sqlite Normal file
View File

@@ -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"]

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
docs/
scripts/
contrib/
.bin/
.github/
*.yaml
*.yml
*.json
*.md
*ignore
Makefile

6
.gitattributes vendored Normal file
View File

@@ -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

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @aeneasr @ory/product-development

8
.github/FUNDING.yml vendored Normal file
View File

@@ -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

122
.github/ISSUE_TEMPLATE/BUG-REPORT.yml vendored Normal file
View File

@@ -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://<your-project-slug>.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

125
.github/ISSUE_TEMPLATE/DESIGN-DOC.yml vendored Normal file
View File

@@ -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://<your-project-slug>.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

View File

@@ -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://<your-project-slug>.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

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -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.

16
.github/auto_assign.yml vendored Normal file
View File

@@ -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

6
.github/config.yml vendored Normal file
View File

@@ -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

22
.github/dependabot.yml vendored Normal file
View File

@@ -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"

12
.github/labels.json vendored Normal file
View File

@@ -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"
}
]

60
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,60 @@
<!--
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
This text will be included in the changelog. If applicable, include links to documentation or pieces of code.
If your change includes breaking changes please add a code block documenting the breaking change:
```
BREAKING CHANGES: This patch changes the behavior of configuration item `foo` to do bar. To keep the existing
behavior please do baz.
```
-->
## Related issue(s)
<!--
If this pull request
1. is a fix for a known bug, link the issue where the bug was reported in the format of `#1234`;
2. is a fix for a previously unknown bug, explain the bug and how to reproduce it in this pull request;
3. implements a new feature, link the issue containing the design document in the format of `#1234`;
4. improves the documentation, no issue reference is required.
Pull requests introducing new features, which do not have a design document linked are more likely to be rejected and take on average 2-8 weeks longer to
get merged.
You can discuss changes with maintainers either in the Github Discussions in this repository or
join the [Ory Chat](https://www.ory.com/chat).
-->
## Checklist
<!--
Put an `x` in the boxes that apply. You can also fill these out after creating the PR.
Please be aware that pull requests must have all boxes ticked in order to be merged.
If you're unsure about any of them, don't hesitate to ask. We're here to help!
-->
- [ ] 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
<!--
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution
you did and what alternatives you considered, etc...
-->

242
.github/workflows/ci.yaml vendored Normal file
View File

@@ -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
---
<!-- This file is generated. Please edit the .proto files instead to update the documentation -->" > "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

30
.github/workflows/closed_references.yml vendored Normal file
View File

@@ -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' }}

72
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -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

View File

@@ -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}"

114
.github/workflows/cve-scan.yaml vendored Normal file
View File

@@ -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::"

22
.github/workflows/format.yml vendored Normal file
View File

@@ -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

25
.github/workflows/labels.yml vendored Normal file
View File

@@ -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

33
.github/workflows/milestone.yml vendored Normal file
View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -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 }}

30
.github/workflows/pm.yml vendored Normal file
View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

47
.github/workflows/stale.yml vendored Normal file
View File

@@ -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"

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.bin/
node_modules/
.idea/
coverage.txt
dist/
**/*.sqlite
**/*.sqlite-journal
.vscode/
.fuzzer/

9
.golangci.yml Normal file
View File

@@ -0,0 +1,9 @@
version: "2"
linters:
enable:
- gosec
- errcheck
- ineffassign
- staticcheck
- unused

16
.goreleaser.yml Normal file
View File

@@ -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"

10
.grype.yaml Normal file
View File

@@ -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

8
.mailmap Normal file
View File

@@ -0,0 +1,8 @@
Aeneas Rekkas <aeneas@ory.sh> <arekkas@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <3372410+aeneasr@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <aeneasr@users.noreply.github.com>
Aeneas Rekkas <aeneas@ory.sh> <aeneas@ory.sh>
Aeneas Rekkas <aeneas@ory.sh> <aeneas@ory.am>
Henning Perl <henning.perl@ory.sh> <henning.perl@gmail.com>
Patrik Neu <patrik@ory.sh> <zepatrik@users.noreply.github.com>
Patrik Neu <patrik@ory.sh> <patrik@ory.sh>

12
.npmignore Normal file
View File

@@ -0,0 +1,12 @@
*.md
*.go
*.php
*.md
*.png
*.jpg
*.jpeg
sdk/go/*
sdk/php/*
docs/*
scripts/*
vendor/*

7
.orycli.yml Normal file
View File

@@ -0,0 +1,7 @@
project: keto
pre_release_hooks:
- ./scripts/render-schemas.sh
ignore_tags:
- proto/.*

9
.prettierignore Normal file
View File

@@ -0,0 +1,9 @@
.bin
.snapshots
internal
cmd
ketoctx
.schema/openapi/patches
proto/**/*.js
proto/**/*.ts
oryx

3
.reference-ignore Normal file
View File

@@ -0,0 +1,3 @@
**/node_modules
docs
CHANGELOG.md

View File

@@ -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"
1 module name licenses
2 github.com/ory/x Apache-2.0
3 github.com/stretchr/testify MIT
4 go.opentelemetry.io/otel/sdk Apache-2.0

5
.schema/README.md Normal file
View File

@@ -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.

508
.schema/config.schema.json Normal file
View File

@@ -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-]+)*))?$"
}
}
}

View File

@@ -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."
}

View File

@@ -0,0 +1,6 @@
disallowAdditionalPropertiesIfNotPresent: true
packageName: client
generateInterfaces: true
isGoSubmodule: false
structPrefix: true
enumClassPrefix: true

View File

@@ -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

View File

@@ -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 }}

View File

@@ -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"

View File

@@ -0,0 +1,7 @@
language: go
install:
- go get -d -v .
script:
- go build -v ./

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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)
}

View File

@@ -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}}

View File

@@ -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}}

View File

@@ -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)
}

View File

@@ -0,0 +1 @@
{{{openapi-yaml}}}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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"]
}
}
}
]
}
]
}

216
.schema/version.schema.json Normal file
View File

@@ -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"
}

58
AUTHORS Normal file
View File

@@ -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@serlo.org>
Aeneas Rekkas <aeneas@ory.sh>
Andrew Minkin <minkin.andrew@gmail.com>
Anirudh Oppiliappan <x@icyphox.sh>
Aravind <aravindrckumar@gmail.com>
Arthur Knoepflin <arthur.knoepflin@epitech.eu>
AVA Monitoring <56961522+avamonitoring@users.noreply.github.com>
Benjie Gillam <benjie@jemjie.com>
Canh Dinh <vancanhuit@gmail.com>
Conor Wyse <conor.wyse@gonitro.com>
csuzhang <48237831+zhyChesterCheung@users.noreply.github.com>
Cypherpunk Samurai <66906402+CypherpunkSamurai@users.noreply.github.com>
Deepak Sharma <sadysnaat@gmail.com>
Dimas Yudha P <prawira.dimas.yudha@gmail.com>
Ferdynand Naczynski <nipsufn+gh@pm.me>
Florian Nagel <nagelflorian@users.noreply.github.com>
fredbi <fredbi@yahoo.com>
Guy Zylberberg <guyzyl@gmail.com>
Henning Perl <henning.perl@ory.sh>
Henry Williams <3443648+henryjw@users.noreply.github.com>
hperl <34397+hperl@users.noreply.github.com>
Ismail Demirbilek <ce.demirbilek@gmail.com>
Jakub Błaszczyk <daemonsthere@gmail.com>
Jan Moxter <jan.moxter@moxter.net>
jingkai <contact@jingk.ai>
Joel Hoisko <joel.hoisko@gmail.com>
John <jfcurran@users.noreply.github.com>
Kevin Goslar <kevin.goslar@gmail.com>
Klaus Herrmann <106238709+kmherrmann@users.noreply.github.com>
Kumbirai Tanekha <kumbirai.tanekha@gmail.com>
Manoel V. Machado <manoel_vilela@engineer.com>
Marc S <cramja@users.noreply.github.com>
Mart Aarma <mart.aarma@nortal.com>
minchao <minchao.220@gmail.com>
Mohammad Aziz <mdaziz067@gmail.com>
Moritz Heiber <github@heiber.im>
nipsufn <nipsufn+gh@pm.me>
Oscar Gómez <oscar.gomez@gmail.com>
Patrik Neu <patrik@ory.sh>
Patryk <patrygo222@gmail.com>
Paul Crussaire <36532199+DevPolo@users.noreply.github.com>
robbietree8 <73832691+robbietree8@users.noreply.github.com>
Robin Brämer <robin.braemer@web.de>
Rob Liebowitz <rliebowitz@morningconsult.com>
Roman Minkin <minkinroman@gmail.com>
Satoshi Matsumoto <kaorimatz@gmail.com>
Shaswat Saxena <shaswatsaxena@gmail.com>
Stuart P. Bentley <s@stuartpb.com>
sumeet jain <sumeet.jain44@gmail.com>
Svante Bengtson <svante@swantzter.se>
Takashi Yoneuchi <takashi.yoneuchi@shift-js.info>
Thomas Aidan Curran <thomasaidancurran@gmail.com>
Tom Fenech <tomjwfenech@gmail.com>
Trang Mai <40815729+trangmaiq@users.noreply.github.com>
Vincent <vincent@ory.sh>

145
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,145 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/CODE_OF_CONDUCT.md -->
# 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

293
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,293 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/CONTRIBUTING.md -->
# Contribute to Ory Keto<!-- omit in toc -->
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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:<YOUR-GITHUB-USERNAME-HERE>/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).

112
DEVELOP.md Normal file
View File

@@ -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
```

201
LICENSE Normal file
View File

@@ -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.

165
Makefile Normal file
View File

@@ -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

617
README.md Normal file
View File

@@ -0,0 +1,617 @@
<h1 align="center">
<img src="https://raw.githubusercontent.com/ory/meta/master/static/banners/keto.svg" alt="Ory Keto - Open Source & Cloud Native Access Control Server">
</h1>
<h4 align="center">
<a href="https://www.ory.com/chat">Chat</a> ·
<a href="https://github.com/ory/keto/discussions">Discussions</a> ·
<a href="https://www.ory.com/l/sign-up-newsletter">Newsletter</a> ·
<a href="https://www.ory.com/docs/">Docs</a> ·
<a href="https://console.ory.sh/">Try Ory Network</a> ·
<a href="https://www.ory.com/jobs/">Jobs</a>
</h4>
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.
---
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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
<!--BEGIN ADOPTERS-->
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
<a href="mailto:office@ory.com">office@ory.com</a> now_!
<table>
<thead>
<tr>
<th>Name</th>
<th>Logo</th>
<th>Website</th>
<th>Case Study</th>
</tr>
</thead>
<tbody>
<tr>
<td>OpenAI</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/openai.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/openai.svg" alt="OpenAI">
</picture>
</td>
<td><a href="https://openai.com/">openai.com</a></td>
<td><a href="https://www.ory.com/case-studies/openai">OpenAI Case Study</a></td>
</tr>
<tr>
<td>Fandom</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/fandom.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/fandom.svg" alt="Fandom">
</picture>
</td>
<td><a href="https://www.fandom.com/">fandom.com</a></td>
<td><a href="https://www.ory.com/case-studies/fandom">Fandom Case Study</a></td>
</tr>
<tr>
<td>Lumin</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/lumin.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/lumin.svg" alt="Lumin">
</picture>
</td>
<td><a href="https://www.luminpdf.com/">luminpdf.com</a></td>
<td><a href="https://www.ory.com/case-studies/lumin">Lumin Case Study</a></td>
</tr>
<tr>
<td>Sencrop</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/sencrop.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/sencrop.svg" alt="Sencrop">
</picture>
</td>
<td><a href="https://sencrop.com/">sencrop.com</a></td>
<td><a href="https://www.ory.com/case-studies/sencrop">Sencrop Case Study</a></td>
</tr>
<tr>
<td>OSINT Industries</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/osint.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/osint.svg" alt="OSINT Industries">
</picture>
</td>
<td><a href="https://www.osint.industries/">osint.industries</a></td>
<td><a href="https://www.ory.com/case-studies/osint">OSINT Industries Case Study</a></td>
</tr>
<tr>
<td>HGV</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hgv.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hgv.svg" alt="HGV">
</picture>
</td>
<td><a href="https://www.hgv.it/">hgv.it</a></td>
<td><a href="https://www.ory.com/case-studies/hgv">HGV Case Study</a></td>
</tr>
<tr>
<td>Maxroll</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/maxroll.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/maxroll.svg" alt="Maxroll">
</picture>
</td>
<td><a href="https://maxroll.gg/">maxroll.gg</a></td>
<td><a href="https://www.ory.com/case-studies/maxroll">Maxroll Case Study</a></td>
</tr>
<tr>
<td>Zezam</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/zezam.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/zezam.svg" alt="Zezam">
</picture>
</td>
<td><a href="https://www.zezam.io/">zezam.io</a></td>
<td><a href="https://www.ory.com/case-studies/zezam">Zezam Case Study</a></td>
</tr>
<tr>
<td>T.RowePrice</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/troweprice.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/troweprice.svg" alt="T.RowePrice">
</picture>
</td>
<td><a href="https://www.troweprice.com/">troweprice.com</a></td>
</tr>
<tr>
<td>Mistral</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/mistral.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/mistral.svg" alt="Mistral">
</picture>
</td>
<td><a href="https://www.mistral.ai/">mistral.ai</a></td>
</tr>
<tr>
<td>Axel Springer</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/axelspringer.svg" />
<img height="22px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/axelspringer.svg" alt="Axel Springer">
</picture>
</td>
<td><a href="https://www.axelspringer.com/">axelspringer.com</a></td>
</tr>
<tr>
<td>Hemnet</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hemnet.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hemnet.svg" alt="Hemnet">
</picture>
</td>
<td><a href="https://www.hemnet.se/">hemnet.se</a></td>
</tr>
<tr>
<td>Cisco</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/cisco.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/cisco.svg" alt="Cisco">
</picture>
</td>
<td><a href="https://www.cisco.com/">cisco.com</a></td>
</tr>
<tr>
<td>Presidencia de la República Dominicana</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/republica-dominicana.svg" />
<img height="42px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/republica-dominicana.svg" alt="Presidencia de la República Dominicana">
</picture>
</td>
<td><a href="https://www.presidencia.gob.do/">presidencia.gob.do</a></td>
</tr>
<tr>
<td>Moonpig</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/moonpig.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/moonpig.svg" alt="Moonpig">
</picture>
</td>
<td><a href="https://www.moonpig.com/">moonpig.com</a></td>
</tr>
<tr>
<td>Booster</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/booster.svg" />
<img height="18px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/booster.svg" alt="Booster">
</picture>
</td>
<td><a href="https://www.choosebooster.com/">choosebooster.com</a></td>
</tr>
<tr>
<td>Zaptec</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/zaptec.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/zaptec.svg" alt="Zaptec">
</picture>
</td>
<td><a href="https://www.zaptec.com/">zaptec.com</a></td>
</tr>
<tr>
<td>Klarna</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/klarna.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/klarna.svg" alt="Klarna">
</picture>
</td>
<td><a href="https://www.klarna.com/">klarna.com</a></td>
</tr>
<tr>
<td>Raspberry PI Foundation</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/raspi.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/raspi.svg" alt="Raspberry PI Foundation">
</picture>
</td>
<td><a href="https://www.raspberrypi.org/">raspberrypi.org</a></td>
</tr>
<tr>
<td>Tulip</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/tulip.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/tulip.svg" alt="Tulip Retail">
</picture>
</td>
<td><a href="https://tulip.com/">tulip.com</a></td>
</tr>
<tr>
<td>Hootsuite</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/hootsuite.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/hootsuite.svg" alt="Hootsuite">
</picture>
</td>
<td><a href="https://hootsuite.com/">hootsuite.com</a></td>
</tr>
<tr>
<td>Segment</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/segment.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/segment.svg" alt="Segment">
</picture>
</td>
<td><a href="https://segment.com/">segment.com</a></td>
</tr>
<tr>
<td>Arduino</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/arduino.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/arduino.svg" alt="Arduino">
</picture>
</td>
<td><a href="https://www.arduino.cc/">arduino.cc</a></td>
</tr>
<tr>
<td>Sainsbury's</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/sainsburys.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/sainsburys.svg" alt="Sainsbury's">
</picture>
</td>
<td><a href="https://www.sainsburys.co.uk/">sainsburys.co.uk</a></td>
</tr>
<tr>
<td>Contraste</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/contraste.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/contraste.svg" alt="Contraste">
</picture>
</td>
<td><a href="https://www.contraste.com/en">contraste.com</a></td>
</tr>
<tr>
<td>inMusic</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/inmusic.svg" />
<img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/inmusic.svg" alt="InMusic">
</picture>
</td>
<td><a href="https://inmusicbrands.com/">inmusicbrands.com</a></td>
</tr>
<tr>
<td>Buhta</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/buhta.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/buhta.svg" alt="Buhta">
</picture>
</td>
<td><a href="https://buhta.com/">buhta.com</a></td>
</tr>
</tr>
<tr>
<td>Amplitude</td>
<td align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/amplitude.svg" />
<img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/amplitude.svg" alt="amplitude.com">
</picture>
</td>
<td><a href="https://amplitude.com/">amplitude.com</a></td>
</tr>
<tr>
<td align="center"><a href="https://tier4.jp/en/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/tieriv.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/tieriv.svg" alt="TIER IV"></picture></a></td>
<td align="center"><a href="https://kyma-project.io"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/kyma.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/kyma.svg" alt="Kyma Project"></picture></a></td>
<td align="center"><a href="https://serlo.org/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/serlo.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/serlo.svg" alt="Serlo"></picture></a></td>
<td align="center"><a href="https://padis.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/padis.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/padis.svg" alt="Padis"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://cloudbear.eu/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/cloudbear.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/cloudbear.svg" alt="Cloudbear"></picture></a></td>
<td align="center"><a href="https://securityonionsolutions.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/securityonion.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/securityonion.svg" alt="Security Onion Solutions"></picture></a></td>
<td align="center"><a href="https://factlylabs.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/factly.svg" /><img height="24px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/factly.svg" alt="Factly"></picture></a></td>
<td align="center"><a href="https://cashdeck.com.au/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/allmyfunds.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/allmyfunds.svg" alt="All My Funds"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://nortal.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/nortal.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/nortal.svg" alt="Nortal"></picture></a></td>
<td align="center"><a href="https://www.ordermygear.com/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/ordermygear.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/ordermygear.svg" alt="OrderMyGear"></picture></a></td>
<td align="center"><a href="https://r2devops.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/r2devops.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/r2devops.svg" alt="R2Devops"></picture></a></td>
<td align="center"><a href="https://www.paralus.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/paralus.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/paralus.svg" alt="Paralus"></picture></a></td>
</tr>
<tr>
<td align="center"><a href="https://dyrector.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/dyrector_io.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/dyrector_io.svg" alt="dyrector.io"></picture></a></td>
<td align="center"><a href="https://pinniped.dev/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/pinniped.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/pinniped.svg" alt="pinniped.dev"></picture></a></td>
<td align="center"><a href="https://pvotal.tech/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ory/meta/master/static/adopters/light/pvotal.svg" /><img height="32px" src="https://raw.githubusercontent.com/ory/meta/master/static/adopters/dark/pvotal.svg" alt="pvotal.tech"></picture></a></td>
<td></td>
</tr>
</tbody>
</table>
Many thanks to all individual contributors
<a href="https://opencollective.com/ory" target="_blank"><img src="https://opencollective.com/ory/contributors.svg?width=890&limit=714&button=false" /></a>
<!--END ADOPTERS-->
## Ecosystem
<!--BEGIN 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.
<!--END ECOSYSTEM-->
## 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.

56
SECURITY.md Normal file
View File

@@ -0,0 +1,56 @@
<!-- AUTO-GENERATED, DO NOT EDIT! -->
<!-- Please edit the original at https://github.com/ory/meta/blob/master/templates/repository/common/SECURITY.md -->
# 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).

222
UPGRADE.md Normal file
View File

@@ -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.
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [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)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## 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[/<id>]` moved to
`/engines/acp/ory/<matching-strategy>/policies[/<id>]`.
- `POST /policies` has been deprecated and merged with `PUT /policies/<id>`
which is now available at
`PUT /engines/acp/ory/<matching-strategy>/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[/<id>]` moved to
`/engines/acp/ory/<matching-strategy>/roles[/<id>]`.
- `POST /roles` has been deprecated and merged with `PUT /roles/<id>` which is
now available at `PUT /engines/acp/ory/<matching-strategy>/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/<id>/members` move to
`/engines/acp/ory/<matching-strategy>/roles/<id>/members`.
- `POST /roles` has been moved to
`PUT /engines/acp/ory/<matching-strategy>/policies/<id>/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/<matching-strategy>/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.

7
benchtest.new.txt Normal file
View File

@@ -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

48
buf.gen.yaml Normal file
View File

@@ -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

12
buf.lock Normal file
View File

@@ -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

27
buf.yaml Normal file
View File

@@ -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

92
cmd/check/root.go Normal file
View File

@@ -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 <subject_namespace>:<subject_id> <relation> <object_namespace>:<object_id>",
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 <subject_namespace>:<subject_id> <relation> <object_namespace> <object_id>' 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())
}

92
cmd/check/root_test.go Normal file
View File

@@ -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 <object_namespace>:<object_id> 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")
})
})
}

21
cmd/clidoc/main.go Normal file
View File

@@ -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.")
}

View File

@@ -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)),
}
}

View File

@@ -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))
}
}

159
cmd/client/grpc_client.go Normal file
View File

@@ -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.")
}

View File

@@ -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")
})
}

87
cmd/expand/root.go Normal file
View File

@@ -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 <relation> <object_namespace>:<object_id>",
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())
}

76
cmd/expand/root_test.go Normal file
View File

@@ -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 <object_namespace>:<object_id> format")
})
}

46
cmd/helpers/helpers.go Normal file
View File

@@ -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 <object_namespace>:<object_id> 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 <object_namespace>:<object_id> format, got %q", args[0])
}
return namespace, object, nil
default:
return "", "", fmt.Errorf("unexpected number of arguments for <object_namespace>:<object_id>: got %d arguments - %s", len(args), strings.Join(args, ","))
}
}

View File

@@ -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)
})
}
}

86
cmd/migrate/down.go Normal file
View File

@@ -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 <steps>",
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 <steps>: %+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
}

181
cmd/migrate/migrate_test.go Normal file
View File

@@ -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))
})
}
}

29
cmd/migrate/root.go Normal file
View File

@@ -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))
}

82
cmd/migrate/status.go Normal file
View File

@@ -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
}

114
cmd/migrate/up.go Normal file
View File

@@ -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
}

Some files were not shown because too many files have changed in this diff Show More