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
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:
22
.docker/Dockerfile
Normal file
22
.docker/Dockerfile
Normal 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
14
.docker/Dockerfile-alpine
Normal 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
38
.docker/Dockerfile-build
Normal 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"]
|
||||
7
.docker/Dockerfile-distroless-static
Normal file
7
.docker/Dockerfile-distroless-static
Normal 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
37
.docker/Dockerfile-sqlite
Normal 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
11
.dockerignore
Normal file
@@ -0,0 +1,11 @@
|
||||
docs/
|
||||
scripts/
|
||||
contrib/
|
||||
.bin/
|
||||
.github/
|
||||
*.yaml
|
||||
*.yml
|
||||
*.json
|
||||
*.md
|
||||
*ignore
|
||||
Makefile
|
||||
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal 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
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @aeneasr @ory/product-development
|
||||
8
.github/FUNDING.yml
vendored
Normal file
8
.github/FUNDING.yml
vendored
Normal 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
122
.github/ISSUE_TEMPLATE/BUG-REPORT.yml
vendored
Normal 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
125
.github/ISSUE_TEMPLATE/DESIGN-DOC.yml
vendored
Normal 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
|
||||
86
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal file
86
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
vendored
Normal 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
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
16
.github/auto_assign.yml
vendored
Normal 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
6
.github/config.yml
vendored
Normal 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
22
.github/dependabot.yml
vendored
Normal 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
12
.github/labels.json
vendored
Normal 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
60
.github/pull_request_template.md
vendored
Normal 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
242
.github/workflows/ci.yaml
vendored
Normal 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
30
.github/workflows/closed_references.yml
vendored
Normal 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
72
.github/workflows/codeql-analysis.yml
vendored
Normal 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
|
||||
59
.github/workflows/conventional_commits.yml
vendored
Normal file
59
.github/workflows/conventional_commits.yml
vendored
Normal 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
114
.github/workflows/cve-scan.yaml
vendored
Normal 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
22
.github/workflows/format.yml
vendored
Normal 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
25
.github/workflows/labels.yml
vendored
Normal 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
33
.github/workflows/milestone.yml
vendored
Normal 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 }}
|
||||
40
.github/workflows/npm_publish_grpc_client.yml
vendored
Normal file
40
.github/workflows/npm_publish_grpc_client.yml
vendored
Normal 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 }}
|
||||
40
.github/workflows/npm_publish_typelib.yml
vendored
Normal file
40
.github/workflows/npm_publish_typelib.yml
vendored
Normal 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
30
.github/workflows/pm.yml
vendored
Normal 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
|
||||
41
.github/workflows/release-go-grpc-client.yml
vendored
Normal file
41
.github/workflows/release-go-grpc-client.yml
vendored
Normal 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 }}
|
||||
30
.github/workflows/single-table-migration-e2e.yml
vendored
Normal file
30
.github/workflows/single-table-migration-e2e.yml
vendored
Normal 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
47
.github/workflows/stale.yml
vendored
Normal 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
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
.bin/
|
||||
node_modules/
|
||||
.idea/
|
||||
coverage.txt
|
||||
dist/
|
||||
**/*.sqlite
|
||||
**/*.sqlite-journal
|
||||
.vscode/
|
||||
.fuzzer/
|
||||
9
.golangci.yml
Normal file
9
.golangci.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gosec
|
||||
- errcheck
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
16
.goreleaser.yml
Normal file
16
.goreleaser.yml
Normal 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
10
.grype.yaml
Normal 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
8
.mailmap
Normal 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
12
.npmignore
Normal file
@@ -0,0 +1,12 @@
|
||||
*.md
|
||||
*.go
|
||||
*.php
|
||||
*.md
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
sdk/go/*
|
||||
sdk/php/*
|
||||
docs/*
|
||||
scripts/*
|
||||
vendor/*
|
||||
7
.orycli.yml
Normal file
7
.orycli.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
project: keto
|
||||
|
||||
pre_release_hooks:
|
||||
- ./scripts/render-schemas.sh
|
||||
|
||||
ignore_tags:
|
||||
- proto/.*
|
||||
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@@ -0,0 +1,9 @@
|
||||
.bin
|
||||
.snapshots
|
||||
internal
|
||||
cmd
|
||||
ketoctx
|
||||
.schema/openapi/patches
|
||||
proto/**/*.js
|
||||
proto/**/*.ts
|
||||
oryx
|
||||
3
.reference-ignore
Normal file
3
.reference-ignore
Normal file
@@ -0,0 +1,3 @@
|
||||
**/node_modules
|
||||
docs
|
||||
CHANGELOG.md
|
||||
6
.reports/dep-licenses.csv
Normal file
6
.reports/dep-licenses.csv
Normal 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"
|
||||
|
||||
|
5
.schema/README.md
Normal file
5
.schema/README.md
Normal 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
508
.schema/config.schema.json
Normal 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-]+)*))?$"
|
||||
}
|
||||
}
|
||||
}
|
||||
5
.schema/namespace.schema.json
Normal file
5
.schema/namespace.schema.json
Normal 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."
|
||||
}
|
||||
6
.schema/openapi/gen.go.yml
Normal file
6
.schema/openapi/gen.go.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
disallowAdditionalPropertiesIfNotPresent: true
|
||||
packageName: client
|
||||
generateInterfaces: true
|
||||
isGoSubmodule: false
|
||||
structPrefix: true
|
||||
enumClassPrefix: true
|
||||
7
.schema/openapi/gen.typescript.yml
Normal file
7
.schema/openapi/gen.typescript.yml
Normal 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
|
||||
94
.schema/openapi/patches/health.yaml
Normal file
94
.schema/openapi/patches/health.yaml
Normal 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 }}
|
||||
12
.schema/openapi/patches/meta.yaml
Normal file
12
.schema/openapi/patches/meta.yaml
Normal 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"
|
||||
7
.schema/openapi/templates/go/.travis.yml
Normal file
7
.schema/openapi/templates/go/.travis.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
language: go
|
||||
|
||||
install:
|
||||
- go get -d -v .
|
||||
|
||||
script:
|
||||
- go build -v ./
|
||||
221
.schema/openapi/templates/go/README.mustache
Normal file
221
.schema/openapi/templates/go/README.mustache
Normal 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}}
|
||||
377
.schema/openapi/templates/go/api.mustache
Normal file
377
.schema/openapi/templates/go/api.mustache
Normal 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}}
|
||||
92
.schema/openapi/templates/go/api_doc.mustache
Normal file
92
.schema/openapi/templates/go/api_doc.mustache
Normal 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}}
|
||||
583
.schema/openapi/templates/go/client.mustache
Normal file
583
.schema/openapi/templates/go/client.mustache
Normal 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
|
||||
}
|
||||
303
.schema/openapi/templates/go/configuration.mustache
Normal file
303
.schema/openapi/templates/go/configuration.mustache
Normal 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)
|
||||
}
|
||||
58
.schema/openapi/templates/go/git_push.sh.mustache
Executable file
58
.schema/openapi/templates/go/git_push.sh.mustache
Executable 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'
|
||||
|
||||
24
.schema/openapi/templates/go/gitignore.mustache
Normal file
24
.schema/openapi/templates/go/gitignore.mustache
Normal 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
|
||||
20
.schema/openapi/templates/go/model.mustache
Normal file
20
.schema/openapi/templates/go/model.mustache
Normal 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}}
|
||||
76
.schema/openapi/templates/go/model_anyof.mustache
Normal file
76
.schema/openapi/templates/go/model_anyof.mustache
Normal 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}}
|
||||
97
.schema/openapi/templates/go/model_doc.mustache
Normal file
97
.schema/openapi/templates/go/model_doc.mustache
Normal 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}}
|
||||
71
.schema/openapi/templates/go/model_enum.mustache
Normal file
71
.schema/openapi/templates/go/model_enum.mustache
Normal 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)
|
||||
}
|
||||
114
.schema/openapi/templates/go/model_oneof.mustache
Normal file
114
.schema/openapi/templates/go/model_oneof.mustache
Normal 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}}
|
||||
391
.schema/openapi/templates/go/model_simple.mustache
Normal file
391
.schema/openapi/templates/go/model_simple.mustache
Normal 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}}
|
||||
35
.schema/openapi/templates/go/nullable_model.mustache
Normal file
35
.schema/openapi/templates/go/nullable_model.mustache
Normal 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)
|
||||
}
|
||||
1
.schema/openapi/templates/go/openapi.mustache
Normal file
1
.schema/openapi/templates/go/openapi.mustache
Normal file
@@ -0,0 +1 @@
|
||||
{{{openapi-yaml}}}
|
||||
18
.schema/openapi/templates/go/partial_header.mustache
Normal file
18
.schema/openapi/templates/go/partial_header.mustache
Normal 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.
|
||||
38
.schema/openapi/templates/go/response.mustache
Normal file
38
.schema/openapi/templates/go/response.mustache
Normal 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
|
||||
}
|
||||
414
.schema/openapi/templates/go/signing.mustache
Normal file
414
.schema/openapi/templates/go/signing.mustache
Normal 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
|
||||
}
|
||||
326
.schema/openapi/templates/go/utils.mustache
Normal file
326
.schema/openapi/templates/go/utils.mustache
Normal 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)
|
||||
}
|
||||
67
.schema/relation_tuple.schema.json
Normal file
67
.schema/relation_tuple.schema.json
Normal 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
216
.schema/version.schema.json
Normal 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
58
AUTHORS
Normal 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
145
CODE_OF_CONDUCT.md
Normal 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
293
CONTRIBUTING.md
Normal 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
112
DEVELOP.md
Normal 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
201
LICENSE
Normal 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
165
Makefile
Normal 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
617
README.md
Normal 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
56
SECURITY.md
Normal 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
222
UPGRADE.md
Normal 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
7
benchtest.new.txt
Normal 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
48
buf.gen.yaml
Normal 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
12
buf.lock
Normal 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
27
buf.yaml
Normal 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
92
cmd/check/root.go
Normal 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
92
cmd/check/root_test.go
Normal 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
21
cmd/clidoc/main.go
Normal 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.")
|
||||
}
|
||||
31
cmd/client/clienttest/fixtures.go
Normal file
31
cmd/client/clienttest/fixtures.go
Normal 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)),
|
||||
}
|
||||
}
|
||||
96
cmd/client/clienttest/test_helpers.go
Normal file
96
cmd/client/clienttest/test_helpers.go
Normal 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
159
cmd/client/grpc_client.go
Normal 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.")
|
||||
}
|
||||
65
cmd/client/grpc_client_test.go
Normal file
65
cmd/client/grpc_client_test.go
Normal 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
87
cmd/expand/root.go
Normal 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
76
cmd/expand/root_test.go
Normal 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
46
cmd/helpers/helpers.go
Normal 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, ","))
|
||||
}
|
||||
}
|
||||
39
cmd/helpers/helpers_test.go
Normal file
39
cmd/helpers/helpers_test.go
Normal 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
86
cmd/migrate/down.go
Normal 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
181
cmd/migrate/migrate_test.go
Normal 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
29
cmd/migrate/root.go
Normal 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
82
cmd/migrate/status.go
Normal 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
114
cmd/migrate/up.go
Normal 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
Reference in New Issue
Block a user