snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (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 tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (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:
8
oryx/.gitignore
vendored
Normal file
8
oryx/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.bin
|
||||
vendor
|
||||
.idea
|
||||
coverage.txt
|
||||
node_modules/
|
||||
**/*.pprof
|
||||
**/memstats.*.txt
|
||||
.vscode/settings.json
|
||||
1
oryx/.goimportsignore
Normal file
1
oryx/.goimportsignore
Normal file
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
9
oryx/.golangci.yml
Normal file
9
oryx/.golangci.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gosec
|
||||
- errcheck
|
||||
- ineffassign
|
||||
- staticcheck
|
||||
- unused
|
||||
0
oryx/.nancy-ignore
Normal file
0
oryx/.nancy-ignore
Normal file
5
oryx/.prettierignore
Normal file
5
oryx/.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
||||
.github/pull_request_template.md
|
||||
clidoc/testdata/
|
||||
healthx/openapi/patch.yaml
|
||||
.snapshots
|
||||
fixtures
|
||||
3
oryx/.reference-ignore
Normal file
3
oryx/.reference-ignore
Normal file
@@ -0,0 +1,3 @@
|
||||
**/node_modules
|
||||
docs
|
||||
CHANGELOG.md
|
||||
92
oryx/.schemas/corsx/viper.schema.json
Normal file
92
oryx/.schemas/corsx/viper.schema.json
Normal file
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/corsx/viper.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Cross Origin Resource Sharing (CORS)",
|
||||
"description": "Configure [Cross Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/) using the following options.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Enable CORS",
|
||||
"description": "If set to true, CORS will be enabled and preflight-requests (OPTION) will be answered."
|
||||
},
|
||||
"allowed_origins": {
|
||||
"title": "Allowed Origins",
|
||||
"description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality. Only one wildcard can be used per origin.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"default": ["*"],
|
||||
"uniqueItems": true,
|
||||
"examples": [
|
||||
"https://example.com",
|
||||
"https://*.example.com",
|
||||
"https://*.foo.example.com"
|
||||
]
|
||||
},
|
||||
"allowed_methods": {
|
||||
"type": "array",
|
||||
"title": "Allowed HTTP Methods",
|
||||
"description": "A list of methods the client is allowed to use with cross-domain requests.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"GET",
|
||||
"HEAD",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"TRACE",
|
||||
"PATCH"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"default": ["GET", "POST", "PUT", "PATCH", "DELETE"]
|
||||
},
|
||||
"allowed_headers": {
|
||||
"description": "A list of non simple headers the client is allowed to use with cross-domain requests.",
|
||||
"title": "Allowed Request HTTP Headers",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minLength": 1,
|
||||
"uniqueItems": true,
|
||||
"default": ["Authorization", "Content-Type"]
|
||||
},
|
||||
"exposed_headers": {
|
||||
"description": "Indicates which headers are safe to expose to the API of a CORS API specification",
|
||||
"title": "Allowed Response HTTP Headers",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"minLength": 1,
|
||||
"uniqueItems": true,
|
||||
"default": ["Content-Type"]
|
||||
},
|
||||
"allow_credentials": {
|
||||
"type": "boolean",
|
||||
"title": "Allow HTTP Credentials",
|
||||
"default": false,
|
||||
"description": "Indicates whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates."
|
||||
},
|
||||
"max_age": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"title": "Maximum Age",
|
||||
"description": "Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0 which stands for no max age."
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Enable Debugging",
|
||||
"description": "Set to true to debug server side CORS issues."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
24
oryx/.schemas/logrusx/viper.schema.json
Normal file
24
oryx/.schemas/logrusx/viper.schema.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/logrusx/viper.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Log",
|
||||
"description": "Configure logging using the following options. Logging will always be sent to stdout and stderr.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "string",
|
||||
"default": "info",
|
||||
"enum": ["panic", "fatal", "error", "warn", "info", "debug"],
|
||||
"title": "Level",
|
||||
"description": "Debug enables stack traces on errors. Can also be set using environment variable LOG_LEVEL."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"default": "text",
|
||||
"enum": ["text", "json"],
|
||||
"title": "Format",
|
||||
"description": "The log format can either be text or JSON."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
8
oryx/.schemas/profilingx/viper.schema.json
Normal file
8
oryx/.schemas/profilingx/viper.schema.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/profilingx/viper.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Profiling",
|
||||
"description": "Enables CPU or memory profiling if set. For more details on profiling Go programs read [Profiling Go Programs](https://blog.golang.org/profiling-go-programs).",
|
||||
"type": "string",
|
||||
"enum": ["cpu", "mem"]
|
||||
}
|
||||
47
oryx/.schemas/tlsx/viper.schema.json
Normal file
47
oryx/.schemas/tlsx/viper.schema.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$id": "https://raw.githubusercontent.com/ory/x/master/.schemas/tlsx/viper.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "HTTPS",
|
||||
"description": "Configure HTTP over TLS (HTTPS). All options can also be set using environment variables by replacing dots (`.`) with underscores (`_`) and uppercasing the key. For example, `some.prefix.tls.key.path` becomes `export SOME_PREFIX_TLS_KEY_PATH`. If all keys are left undefined, TLS will be disabled.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"source": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"path": {
|
||||
"title": "Path to PEM-encoded Fle",
|
||||
"type": "string",
|
||||
"examples": ["path/to/file.pem"]
|
||||
},
|
||||
"base64": {
|
||||
"title": "Base64 Encoded Inline",
|
||||
"description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"key": {
|
||||
"title": "Private Key (PEM)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/source"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cert": {
|
||||
"title": "TLS Certificate (PEM)",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/source"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
201
oryx/LICENSE
Normal file
201
oryx/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.
|
||||
61
oryx/Makefile
Normal file
61
oryx/Makefile
Normal file
@@ -0,0 +1,61 @@
|
||||
SHELL=/bin/bash -o pipefail
|
||||
|
||||
export PATH := .bin:${PATH}
|
||||
|
||||
.bin/ory: Makefile
|
||||
curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/meta/master/install.sh | bash -s -- -b .bin ory v0.2.2
|
||||
touch .bin/ory
|
||||
|
||||
.PHONY: format
|
||||
format: .bin/ory node_modules
|
||||
.bin/ory dev headers copyright --type=open-source --exclude=clidoc/ --exclude=hasherx/mocks_pkdbf2_test.go --exclude=josex/ --exclude=hasherx/ --exclude=jsonnetsecure/jsonnet.go
|
||||
go tool goimports -w -local github.com/ory .
|
||||
npm exec -- prettier --write .
|
||||
|
||||
.bin/golangci-lint: Makefile
|
||||
curl --retry 7 --retry-connrefused -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin v2.10.1
|
||||
|
||||
.bin/licenses: Makefile
|
||||
curl --retry 7 --retry-connrefused https://raw.githubusercontent.com/ory/ci/master/licenses/install | sh
|
||||
|
||||
licenses: .bin/licenses node_modules # checks open-source licenses
|
||||
.bin/licenses
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
make resetdb
|
||||
export TEST_DATABASE_POSTGRESQL=postgres://postgres:secret@127.0.0.1:3445/hydra?sslmode=disable; export TEST_DATABASE_COCKROACHDB=cockroach://root@127.0.0.1:3446/defaultdb?sslmode=disable; export TEST_DATABASE_MYSQL='mysql://root:secret@tcp(127.0.0.1:3444)/mysql?parseTime=true&multiStatements=true'; go test -count=1 ./...
|
||||
|
||||
.PHONY: resetdb
|
||||
resetdb:
|
||||
docker kill hydra_test_database_mysql || true
|
||||
docker kill hydra_test_database_postgres || true
|
||||
docker kill hydra_test_database_cockroach || true
|
||||
docker rm -f hydra_test_database_mysql || true
|
||||
docker rm -f hydra_test_database_postgres || true
|
||||
docker rm -f hydra_test_database_cockroach || true
|
||||
docker run --rm --name hydra_test_database_mysql -p 3444:3306 -e MYSQL_ROOT_PASSWORD=secret -d mysql:8.0
|
||||
docker run --rm --name hydra_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=hydra -d postgres:11.8
|
||||
docker run --rm --name hydra_test_database_cockroach -p 3446:26257 -d cockroachdb/cockroach:latest-v25.4 start-single-node --insecure
|
||||
|
||||
.PHONY: lint
|
||||
lint: .bin/golangci-lint
|
||||
.bin/golangci-lint run -v ./...
|
||||
|
||||
.PHONY: migrations-render
|
||||
migrations-render: .bin/ory
|
||||
ory dev pop migration render networkx/migrations/templates networkx/migrations/sql
|
||||
|
||||
.PHONY: migrations-render-replace
|
||||
migrations-render-replace: .bin/ory
|
||||
ory dev pop migration render -r networkx/migrations/templates networkx/migrations/sql
|
||||
|
||||
.PHONY: mocks
|
||||
mocks:
|
||||
go tool mockgen -package hasherx_test -destination hasherx/mocks_argon2_test.go github.com/ory/x/hasherx Argon2Configurator
|
||||
go tool mockgen -package hasherx_test -destination hasherx/mocks_bcrypt_test.go github.com/ory/x/hasherx BCryptConfigurator
|
||||
go tool mockgen -package hasherx_test -destination hasherx/mocks_pkdbf2_test.go github.com/ory/x/hasherx PBKDF2Configurator
|
||||
|
||||
node_modules: package-lock.json
|
||||
npm ci
|
||||
touch node_modules
|
||||
23
oryx/aead/aead_oss.go
Normal file
23
oryx/aead/aead_oss.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//go:build !commercial
|
||||
|
||||
// Package aead provides the authenticated cipher (AEAD) that Ory uses to
|
||||
// seal opaque payloads such as pagination tokens.
|
||||
package aead
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// New returns the XChaCha20-Poly1305 AEAD that seals opaque payloads under
|
||||
// the given key. Its 192-bit random nonce imposes no practical limit on the
|
||||
// number of payloads sealed per key.
|
||||
func New(key [32]byte) (cipher.AEAD, error) {
|
||||
a, err := chacha20poly1305.NewX(key[:])
|
||||
return a, errors.Wrap(err, "cannot create XChaCha20-Poly1305 AEAD")
|
||||
}
|
||||
19
oryx/aead/legacy_secretbox.go
Normal file
19
oryx/aead/legacy_secretbox.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package aead
|
||||
|
||||
import "golang.org/x/crypto/nacl/secretbox"
|
||||
|
||||
// OpenLegacySecretbox opens a payload sealed by the NaCl secretbox scheme
|
||||
// that was used before this package existed: a 24-byte nonce prefixed to the
|
||||
// ciphertext, without additional data. Remove it once all such payloads have
|
||||
// expired.
|
||||
func OpenLegacySecretbox(key [32]byte, raw []byte) ([]byte, bool) {
|
||||
if len(raw) < 24 {
|
||||
return nil, false
|
||||
}
|
||||
var nonce [24]byte
|
||||
copy(nonce[:], raw[:24])
|
||||
return secretbox.Open(nil, raw[24:], &nonce, &key)
|
||||
}
|
||||
58
oryx/assertx/assertx.go
Normal file
58
oryx/assertx/assertx.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package assertx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func PrettifyJSONPayload(t testing.TB, payload interface{}) string {
|
||||
t.Helper()
|
||||
o, err := json.MarshalIndent(payload, "", " ")
|
||||
require.NoErrorf(t, err, "%+v", payload)
|
||||
return string(o)
|
||||
}
|
||||
|
||||
func EqualAsJSON(t testing.TB, expected, actual interface{}, args ...interface{}) {
|
||||
t.Helper()
|
||||
var eb, ab bytes.Buffer
|
||||
if len(args) == 0 {
|
||||
args = []interface{}{PrettifyJSONPayload(t, actual)}
|
||||
}
|
||||
|
||||
require.NoError(t, json.NewEncoder(&eb).Encode(expected), args...)
|
||||
require.NoError(t, json.NewEncoder(&ab).Encode(actual), args...)
|
||||
assert.JSONEq(t, strings.TrimSpace(eb.String()), strings.TrimSpace(ab.String()), args...)
|
||||
}
|
||||
|
||||
func EqualAsJSONExcept(t testing.TB, expected, actual interface{}, except []string, args ...interface{}) {
|
||||
t.Helper()
|
||||
var eb, ab bytes.Buffer
|
||||
if len(args) == 0 {
|
||||
args = []interface{}{PrettifyJSONPayload(t, actual)}
|
||||
}
|
||||
|
||||
require.NoError(t, json.NewEncoder(&eb).Encode(expected), args...)
|
||||
require.NoError(t, json.NewEncoder(&ab).Encode(actual), args...)
|
||||
|
||||
var err error
|
||||
ebs, abs := eb.String(), ab.String()
|
||||
for _, k := range except {
|
||||
ebs, err = sjson.Delete(ebs, k)
|
||||
require.NoError(t, err)
|
||||
|
||||
abs, err = sjson.Delete(abs, k)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.JSONEq(t, strings.TrimSpace(ebs), strings.TrimSpace(abs), args...)
|
||||
}
|
||||
30
oryx/cachex/cache_control.go
Normal file
30
oryx/cachex/cache_control.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cachex
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type CacheControl map[string]string
|
||||
|
||||
// From Go generated SDK code.
|
||||
func ParseCacheControl(headers http.Header) CacheControl {
|
||||
cc := CacheControl{}
|
||||
ccHeader := headers.Get("Cache-Control")
|
||||
for _, part := range strings.Split(ccHeader, ",") {
|
||||
part = strings.Trim(part, " ")
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
if strings.ContainsRune(part, '=') {
|
||||
keyval := strings.Split(part, "=")
|
||||
cc[strings.Trim(keyval[0], " ")] = strings.Trim(keyval[1], ",")
|
||||
} else {
|
||||
cc[part] = ""
|
||||
}
|
||||
}
|
||||
return cc
|
||||
}
|
||||
67
oryx/cachex/ristretto.go
Normal file
67
oryx/cachex/ristretto.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cachex
|
||||
|
||||
import (
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// RistrettoCollector collects Ristretto cache metrics.
|
||||
type RistrettoCollector struct {
|
||||
prefix string
|
||||
metricsFunc func() *ristretto.Metrics
|
||||
}
|
||||
|
||||
// NewRistrettoCollector creates a new RistrettoCollector.
|
||||
//
|
||||
// To use this collector, you need to register it with a Prometheus registry:
|
||||
//
|
||||
// func main() {
|
||||
// cache, _ := ristretto.NewCache(&ristretto.Config{
|
||||
// NumCounters: 1e7,
|
||||
// MaxCost: 1 << 30,
|
||||
// BufferItems: 64,
|
||||
// })
|
||||
// collector := NewRistrettoCollector("prefix_", func() *ristretto.Metrics {
|
||||
// return cache.Metrics
|
||||
// })
|
||||
// prometheus.MustRegister(collector)
|
||||
// }
|
||||
func NewRistrettoCollector(prefix string, metricsFunc func() *ristretto.Metrics) *RistrettoCollector {
|
||||
return &RistrettoCollector{
|
||||
prefix: prefix,
|
||||
metricsFunc: metricsFunc,
|
||||
}
|
||||
}
|
||||
|
||||
// Describe sends the super-set of all possible descriptors of metrics
|
||||
// collected by this Collector.
|
||||
func (c *RistrettoCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_hits", "Total number of cache hits", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_misses", "Total number of cache misses", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_ratio", "Cache hit ratio", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_keys_added", "Total number of keys added to the cache", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_cost_added", "Total cost of keys added to the cache", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_keys_evicted", "Total number of keys evicted from the cache", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_cost_evicted", "Total cost of keys evicted from the cache", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_sets_dropped", "Total number of sets dropped", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_sets_rejected", "Total number of sets rejected", nil, nil)
|
||||
ch <- prometheus.NewDesc(c.prefix+"ristretto_gets_kept", "Total number of gets kept", nil, nil)
|
||||
}
|
||||
|
||||
// Collect is called by the Prometheus registry when collecting metrics.
|
||||
func (c *RistrettoCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
metrics := c.metricsFunc()
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_hits", "Total number of cache hits", nil, nil), prometheus.GaugeValue, float64(metrics.Hits()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_misses", "Total number of cache misses", nil, nil), prometheus.GaugeValue, float64(metrics.Misses()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_ratio", "Cache hit ratio", nil, nil), prometheus.GaugeValue, metrics.Ratio())
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_keys_added", "Total number of keys added to the cache", nil, nil), prometheus.GaugeValue, float64(metrics.KeysAdded()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_cost_added", "Total cost of keys added to the cache", nil, nil), prometheus.GaugeValue, float64(metrics.CostAdded()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_keys_evicted", "Total number of keys evicted from the cache", nil, nil), prometheus.GaugeValue, float64(metrics.KeysEvicted()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_cost_evicted", "Total cost of keys evicted from the cache", nil, nil), prometheus.GaugeValue, float64(metrics.CostEvicted()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_sets_dropped", "Total number of sets dropped", nil, nil), prometheus.GaugeValue, float64(metrics.SetsDropped()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_sets_rejected", "Total number of sets rejected", nil, nil), prometheus.GaugeValue, float64(metrics.SetsRejected()))
|
||||
ch <- prometheus.MustNewConstMetric(prometheus.NewDesc(c.prefix+"ristretto_gets_kept", "Total number of gets kept", nil, nil), prometheus.GaugeValue, float64(metrics.GetsKept()))
|
||||
}
|
||||
68
oryx/castx/castx.go
Normal file
68
oryx/castx/castx.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package castx
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
// ToFloatSlice casts an interface to a []float64 type.
|
||||
func ToFloatSlice(i interface{}) []float64 {
|
||||
f, _ := ToFloatSliceE(i)
|
||||
return f
|
||||
}
|
||||
|
||||
// ToFloatSliceE casts an interface to a []float64 type.
|
||||
func ToFloatSliceE(i interface{}) ([]float64, error) {
|
||||
if i == nil {
|
||||
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i)
|
||||
}
|
||||
|
||||
switch v := i.(type) {
|
||||
case []float64:
|
||||
return v, nil
|
||||
}
|
||||
|
||||
kind := reflect.TypeOf(i).Kind()
|
||||
switch kind {
|
||||
case reflect.Slice, reflect.Array:
|
||||
s := reflect.ValueOf(i)
|
||||
a := make([]float64, s.Len())
|
||||
for j := range a {
|
||||
val, err := cast.ToFloat64E(s.Index(j).Interface())
|
||||
if err != nil {
|
||||
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i)
|
||||
}
|
||||
a[j] = val
|
||||
}
|
||||
return a, nil
|
||||
default:
|
||||
return []float64{}, fmt.Errorf("unable to cast %#v of type %T to []float64", i, i)
|
||||
}
|
||||
}
|
||||
|
||||
// ToStringSlice casts an interface to a []string type and respects comma-separated values.
|
||||
func ToStringSlice(i interface{}) []string {
|
||||
s, _ := ToStringSliceE(i)
|
||||
return s
|
||||
}
|
||||
|
||||
// ToStringSliceE casts an interface to a []string type and respects comma-separated values.
|
||||
func ToStringSliceE(i interface{}) ([]string, error) {
|
||||
switch s := i.(type) {
|
||||
case string:
|
||||
return parseCSV(s)
|
||||
}
|
||||
|
||||
return cast.ToStringSliceE(i)
|
||||
}
|
||||
|
||||
func parseCSV(v string) ([]string, error) {
|
||||
return csv.NewReader(strings.NewReader(v)).Read()
|
||||
}
|
||||
78
oryx/clidoc/generate.go
Normal file
78
oryx/clidoc/generate.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package clidoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Generate generates markdown documentation for a cobra command and its children.
|
||||
func Generate(cmd *cobra.Command, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("command expects one argument which is the path to the output directory")
|
||||
}
|
||||
|
||||
return generate(cmd, args[0])
|
||||
}
|
||||
|
||||
func trimExt(s string) string {
|
||||
return strings.ReplaceAll(strings.TrimSuffix(s, filepath.Ext(s)), "_", "-")
|
||||
}
|
||||
|
||||
func generate(cmd *cobra.Command, dir string) error {
|
||||
cmd.DisableAutoGenTag = true
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
if err := generate(c, dir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
basename := strings.Replace(cmd.CommandPath(), " ", "-", -1)
|
||||
if err := os.MkdirAll(filepath.Join(dir), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := filepath.Join(dir, basename) + ".md"
|
||||
f, err := os.Create(filename) //#nosec:G304
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer (func() { _ = f.Close() })()
|
||||
|
||||
if _, err := io.WriteString(f, fmt.Sprintf(`---
|
||||
id: %s
|
||||
title: %s
|
||||
description: %s
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
`,
|
||||
basename,
|
||||
cmd.CommandPath(),
|
||||
cmd.CommandPath(),
|
||||
)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := GenMarkdownCustom(cmd, &b, trimExt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.WriteString(b.String())
|
||||
return err
|
||||
}
|
||||
201
oryx/clidoc/md_docs.go
Normal file
201
oryx/clidoc/md_docs.go
Normal file
@@ -0,0 +1,201 @@
|
||||
//Copyright 2015 Red Hat Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clidoc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/x/cmdx"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func printOptions(buf *bytes.Buffer, cmd *cobra.Command, name string) error {
|
||||
flags := cmd.NonInheritedFlags()
|
||||
flags.SetOutput(buf)
|
||||
if flags.HasAvailableFlags() {
|
||||
buf.WriteString("### Options\n\n```\n")
|
||||
flags.PrintDefaults()
|
||||
buf.WriteString("```\n\n")
|
||||
}
|
||||
|
||||
parentFlags := cmd.InheritedFlags()
|
||||
parentFlags.SetOutput(buf)
|
||||
if parentFlags.HasAvailableFlags() {
|
||||
buf.WriteString("### Options inherited from parent commands\n\n```\n")
|
||||
parentFlags.PrintDefaults()
|
||||
buf.WriteString("```\n\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenMarkdown creates markdown output.
|
||||
func GenMarkdown(cmd *cobra.Command, w io.Writer) error {
|
||||
return GenMarkdownCustom(cmd, w, func(s string) string { return s })
|
||||
}
|
||||
|
||||
// GenMarkdownCustom creates custom markdown output.
|
||||
func GenMarkdownCustom(cmd *cobra.Command, w io.Writer, linkHandler func(string) string) error {
|
||||
cmd.InitDefaultHelpCmd()
|
||||
cmd.InitDefaultHelpFlag()
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
name := cmd.CommandPath()
|
||||
|
||||
buf.WriteString("## " + html.EscapeString(name) + "\n\n")
|
||||
buf.WriteString(fenceIndentedBlocks(cmd.Short) + "\n\n")
|
||||
if len(cmd.Long) > 0 {
|
||||
buf.WriteString("### Synopsis\n\n")
|
||||
long, err := cmdx.TemplateCommandField(cmd, cmd.Long)
|
||||
if err != nil {
|
||||
buf.WriteString(fmt.Sprintf("<!-- Error rendering cmd.Long: %s -->\n\n", err.Error()))
|
||||
long = cmd.Long
|
||||
}
|
||||
buf.WriteString(fenceIndentedBlocks(long) + "\n\n")
|
||||
}
|
||||
|
||||
if cmd.Runnable() {
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", cmd.UseLine()))
|
||||
}
|
||||
|
||||
if len(cmd.Example) > 0 {
|
||||
buf.WriteString("### Examples\n\n")
|
||||
example, err := cmdx.TemplateCommandField(cmd, cmd.Example)
|
||||
if err != nil {
|
||||
buf.WriteString(fmt.Sprintf("<!-- Error rendering cmd.Example: %s -->\n\n", err.Error()))
|
||||
example = cmd.Example
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("```\n%s\n```\n\n", example))
|
||||
}
|
||||
|
||||
if err := printOptions(buf, cmd, name); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasSeeAlso(cmd) {
|
||||
buf.WriteString("### See also\n\n")
|
||||
if cmd.HasParent() {
|
||||
parent := cmd.Parent()
|
||||
pname := parent.CommandPath()
|
||||
link := pname + ".md"
|
||||
link = strings.Replace(link, " ", "_", -1)
|
||||
short := parent.Short
|
||||
if short != "" {
|
||||
short = fmt.Sprintf(" %s", short)
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("* [%s](%s)%s\n", pname, linkHandler(link), short))
|
||||
cmd.VisitParents(func(c *cobra.Command) {
|
||||
if c.DisableAutoGenTag {
|
||||
cmd.DisableAutoGenTag = c.DisableAutoGenTag
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
children := cmd.Commands()
|
||||
sort.Sort(byName(children))
|
||||
|
||||
for _, child := range children {
|
||||
if !child.IsAvailableCommand() || child.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
cname := name + " " + child.Name()
|
||||
link := cname + ".md"
|
||||
link = strings.Replace(link, " ", "_", -1)
|
||||
short := child.Short
|
||||
if short != "" {
|
||||
short = fmt.Sprintf(" - %s", short)
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("* [%s](%s)\t%s\n", cname, linkHandler(link), short))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
|
||||
_, err := buf.WriteTo(w)
|
||||
return err
|
||||
}
|
||||
|
||||
// GenMarkdownTree will generate a markdown page for this command and all
|
||||
// descendants in the directory given. The header may be nil.
|
||||
// This function may not work correctly if your command names have `-` in them.
|
||||
// If you have `cmd` with two subcmds, `sub` and `sub-third`,
|
||||
// and `sub` has a subcommand called `third`, it is undefined which
|
||||
// help output will be in the file `cmd-sub-third.1`.
|
||||
func GenMarkdownTree(cmd *cobra.Command, dir string) error {
|
||||
identity := func(s string) string { return s }
|
||||
emptyStr := func(s string) string { return "" }
|
||||
return GenMarkdownTreeCustom(cmd, dir, emptyStr, identity)
|
||||
}
|
||||
|
||||
// GenMarkdownTreeCustom is the the same as GenMarkdownTree, but
|
||||
// with custom filePrepender and linkHandler.
|
||||
func GenMarkdownTreeCustom(cmd *cobra.Command, dir string, filePrepender, linkHandler func(string) string) error {
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
if err := GenMarkdownTreeCustom(c, dir, filePrepender, linkHandler); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
basename := strings.Replace(cmd.CommandPath(), " ", "_", -1) + ".md"
|
||||
filename := filepath.Join(dir, basename)
|
||||
f, err := os.Create(filename) //#nosec:G304) //#nosec:G304
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer (func() { _ = f.Close() })()
|
||||
|
||||
if _, err := io.WriteString(f, filePrepender(filename)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := GenMarkdownCustom(cmd, f, linkHandler); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var indentedBlock = regexp.MustCompile(`(?m)^(?: {4}|\t).*(?:\n(?: {4}|\t).*)*`)
|
||||
|
||||
func fenceIndentedBlocks(s string) string {
|
||||
return indentedBlock.ReplaceAllStringFunc(s, func(block string) string {
|
||||
// trim trailing newline to keep fence tidy
|
||||
block = strings.TrimRight(block, "\n")
|
||||
|
||||
// de-indent exactly one level for nicer fenced output
|
||||
lines := strings.Split(block, "\n")
|
||||
for i, ln := range lines {
|
||||
switch {
|
||||
case strings.HasPrefix(ln, " "):
|
||||
lines[i] = ln[4:]
|
||||
case strings.HasPrefix(ln, "\t"):
|
||||
lines[i] = ln[1:]
|
||||
}
|
||||
}
|
||||
b := strings.Join(lines, "\n")
|
||||
|
||||
// guard against already fenced content
|
||||
if strings.HasPrefix(strings.TrimSpace(b), "```") {
|
||||
return block
|
||||
}
|
||||
return "```\n" + b + "\n```"
|
||||
})
|
||||
}
|
||||
48
oryx/clidoc/testdata/hydra-client-admin.md
vendored
Normal file
48
oryx/clidoc/testdata/hydra-client-admin.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: hydra-client-admin
|
||||
title: hydra client admin
|
||||
description: hydra client admin
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
## hydra client admin
|
||||
|
||||
Foo bar baz bar
|
||||
|
||||
```
|
||||
short with multiple
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Synopsis
|
||||
|
||||
Run the admin server
|
||||
|
||||
```
|
||||
<[some argument]>
|
||||
<[some argument]>
|
||||
<[some argument]>
|
||||
<[some argument]>
|
||||
<[some argument]>
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
hydra client admin <args> [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for admin
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
* [hydra client](hydra-client) Run client commands
|
||||
|
||||
36
oryx/clidoc/testdata/hydra-client-public.md
vendored
Normal file
36
oryx/clidoc/testdata/hydra-client-public.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
id: hydra-client-public
|
||||
title: hydra client public
|
||||
description: hydra client public
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
## hydra client public
|
||||
|
||||
|
||||
|
||||
### Synopsis
|
||||
|
||||
Run the public server
|
||||
|
||||
<[some argument]>
|
||||
|
||||
|
||||
```
|
||||
hydra client public <args> [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for public
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
* [hydra client](hydra-client) Run client commands
|
||||
|
||||
48
oryx/clidoc/testdata/hydra-client.md
vendored
Normal file
48
oryx/clidoc/testdata/hydra-client.md
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: hydra-client
|
||||
title: hydra client
|
||||
description: hydra client
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
## hydra client
|
||||
|
||||
Run client commands
|
||||
|
||||
### Synopsis
|
||||
|
||||
Manage OAuth2 clients
|
||||
|
||||
<[some argument]>
|
||||
|
||||
|
||||
```
|
||||
hydra client [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
hydra client --whatever
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for client
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
* [hydra](hydra)
|
||||
* [hydra client admin](hydra-client-admin) - Foo bar baz bar
|
||||
|
||||
short with multiple
|
||||
|
||||
|
||||
* [hydra client public](hydra-client-public)
|
||||
|
||||
36
oryx/clidoc/testdata/hydra-serve.md
vendored
Normal file
36
oryx/clidoc/testdata/hydra-serve.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
id: hydra-serve
|
||||
title: hydra serve
|
||||
description: hydra serve
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
## hydra serve
|
||||
|
||||
|
||||
|
||||
### Synopsis
|
||||
|
||||
Manage the server
|
||||
|
||||
<[some argument]>
|
||||
|
||||
|
||||
```
|
||||
hydra serve [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for serve
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
* [hydra](hydra)
|
||||
|
||||
38
oryx/clidoc/testdata/hydra.md
vendored
Normal file
38
oryx/clidoc/testdata/hydra.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
id: hydra
|
||||
title: hydra
|
||||
description: hydra
|
||||
---
|
||||
|
||||
<!--
|
||||
This file is auto-generated.
|
||||
|
||||
To improve this file please make your change against the appropriate "./cmd/*.go" file.
|
||||
-->
|
||||
## hydra
|
||||
|
||||
|
||||
|
||||
### Synopsis
|
||||
|
||||
A sample text
|
||||
root
|
||||
|
||||
<[some argument]>
|
||||
|
||||
|
||||
```
|
||||
hydra [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for hydra
|
||||
```
|
||||
|
||||
### See also
|
||||
|
||||
* [hydra client](hydra-client) - Run client commands
|
||||
* [hydra serve](hydra-serve)
|
||||
|
||||
40
oryx/clidoc/util.go
Normal file
40
oryx/clidoc/util.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2015 Red Hat Inc. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package clidoc
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Test to see if we have a reason to print See Also information in docs
|
||||
// Basically this is a test for a parent command or a subcommand which is
|
||||
// both not deprecated and not the autogenerated help command.
|
||||
func hasSeeAlso(cmd *cobra.Command) bool {
|
||||
if cmd.HasParent() {
|
||||
return true
|
||||
}
|
||||
for _, c := range cmd.Commands() {
|
||||
if !c.IsAvailableCommand() || c.IsAdditionalHelpTopicCommand() {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type byName []*cobra.Command
|
||||
|
||||
func (s byName) Len() int { return len(s) }
|
||||
func (s byName) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s byName) Less(i, j int) bool { return s[i].Name() < s[j].Name() }
|
||||
65
oryx/clock/clock.go
Normal file
65
oryx/clock/clock.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package clock provides a small, injectable time source. Production code reads
|
||||
// the current time through a Clock instead of calling time.Now() directly, so
|
||||
// time-dependent behavior (such as flow expiry) can be tested deterministically.
|
||||
package clock
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Clock is a source of the current time.
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
|
||||
// Provider is implemented by dependency containers (such as the Kratos registry)
|
||||
// that expose a Clock. Embed it in a component's dependency interface to read
|
||||
// the current time through the injected clock.
|
||||
type Provider interface {
|
||||
Clock() Clock
|
||||
}
|
||||
|
||||
// System is the production Clock backed by the real wall clock.
|
||||
type System struct{}
|
||||
|
||||
// Now returns the current wall-clock time.
|
||||
func (System) Now() time.Time { return time.Now() }
|
||||
|
||||
// New returns the production system clock.
|
||||
func New() Clock { return System{} }
|
||||
|
||||
// Mock is a Clock whose time only changes when the caller advances it. It is
|
||||
// safe for concurrent use: a test goroutine may Add while a request goroutine
|
||||
// reads Now.
|
||||
type Mock struct {
|
||||
mu sync.Mutex
|
||||
t time.Time
|
||||
}
|
||||
|
||||
// NewMock returns a Mock fixed at the given instant.
|
||||
func NewMock(t time.Time) *Mock { return &Mock{t: t} }
|
||||
|
||||
// Now returns the Mock's current instant.
|
||||
func (m *Mock) Now() time.Time {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.t
|
||||
}
|
||||
|
||||
// Add advances the Mock by d.
|
||||
func (m *Mock) Add(d time.Duration) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.t = m.t.Add(d)
|
||||
}
|
||||
|
||||
// Set moves the Mock to the given instant.
|
||||
func (m *Mock) Set(t time.Time) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.t = t
|
||||
}
|
||||
31
oryx/cmdx/env.go
Normal file
31
oryx/cmdx/env.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
// EnvVarExamplesHelpMessage returns a string containing documentation on how to use environment variables.
|
||||
func EnvVarExamplesHelpMessage(name string) string {
|
||||
return `This command exposes a variety of controls via environment variables. Here are some examples on how to
|
||||
configure environment variables:
|
||||
|
||||
Linux / macOS:
|
||||
$ export FOO=bar
|
||||
$ export BAZ=bar
|
||||
$ ` + name + ` ...
|
||||
|
||||
$ FOO=bar BAZ=bar ` + name + ` ...
|
||||
|
||||
Docker:
|
||||
$ docker run -e FOO=bar -e BAZ=bar ...
|
||||
|
||||
Windows (cmd):
|
||||
> set FOO=bar
|
||||
> set BAZ=bar
|
||||
> ` + name + ` ...
|
||||
|
||||
Windows (powershell):
|
||||
> $env:FOO = "bar"
|
||||
> $env:BAZ = "bar"
|
||||
> ` + name + `
|
||||
`
|
||||
}
|
||||
247
oryx/cmdx/helper.go
Normal file
247
oryx/cmdx/helper.go
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoPrintButFail is returned to detect a failure state that was already reported to the user in some way
|
||||
ErrNoPrintButFail = fmt.Errorf("this error should never be printed")
|
||||
|
||||
debugStdout, debugStderr = io.Discard, io.Discard
|
||||
)
|
||||
|
||||
func init() {
|
||||
if os.Getenv("DEBUG") != "" {
|
||||
debugStdout = os.Stdout
|
||||
debugStderr = os.Stderr
|
||||
}
|
||||
}
|
||||
|
||||
// FailSilently is supposed to be used within a commands RunE function.
|
||||
// It silences cobras error handling and returns the ErrNoPrintButFail error.
|
||||
func FailSilently(cmd *cobra.Command) error {
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SilenceUsage = true
|
||||
return errors.WithStack(ErrNoPrintButFail)
|
||||
}
|
||||
|
||||
// Must fatals with the optional message if err is not nil.
|
||||
// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error.
|
||||
func Must(err error, message string, args ...interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(os.Stderr, message+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// CheckResponse fatals if err is nil or the response.StatusCode does not match the expectedStatusCode
|
||||
// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error.
|
||||
func CheckResponse(err error, expectedStatusCode int, response *http.Response) {
|
||||
Must(err, "Command failed because error occurred: %s", err)
|
||||
|
||||
if response.StatusCode != expectedStatusCode {
|
||||
out, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
out = []byte{}
|
||||
}
|
||||
pretty, err := json.MarshalIndent(json.RawMessage(out), "", "\t")
|
||||
if err == nil {
|
||||
out = pretty
|
||||
}
|
||||
|
||||
Fatalf(
|
||||
`Command failed because status code %d was expected but code %d was received.
|
||||
|
||||
Response payload:
|
||||
|
||||
%s`,
|
||||
expectedStatusCode,
|
||||
response.StatusCode,
|
||||
out,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// FormatResponse takes an object and prints a json.MarshalIdent version of it or fatals.
|
||||
// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error.
|
||||
func FormatResponse(o interface{}) string {
|
||||
out, err := json.MarshalIndent(o, "", "\t")
|
||||
Must(err, `Command failed because an error occurred while prettifying output: %s`, err)
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// Fatalf prints to os.Stderr and exists with code 1.
|
||||
// Deprecated: do not use this function in commands, as it makes it impossible to test them. Instead, return the error.
|
||||
func Fatalf(message string, args ...interface{}) {
|
||||
if len(args) > 0 {
|
||||
_, _ = fmt.Fprintf(os.Stderr, message+"\n", args...)
|
||||
} else {
|
||||
_, _ = fmt.Fprintln(os.Stderr, message)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// CallbackWriter will execute each callback once the message is received.
|
||||
// The full matched message is passed to the callback. An error returned from the callback is returned by Write.
|
||||
type CallbackWriter struct {
|
||||
Callbacks map[string]func([]byte) error
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (c *CallbackWriter) Write(msg []byte) (int, error) {
|
||||
for p, cb := range c.Callbacks {
|
||||
if bytes.Contains(msg, []byte(p)) {
|
||||
if err := cb(msg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return c.buf.Write(msg)
|
||||
}
|
||||
|
||||
func (c *CallbackWriter) String() string {
|
||||
return c.buf.String()
|
||||
}
|
||||
|
||||
var _ io.Writer = (*CallbackWriter)(nil)
|
||||
|
||||
func prepareCmd(cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args []string) {
|
||||
cmd.SetIn(stdIn)
|
||||
outs := []io.Writer{debugStdout}
|
||||
if stdOut != nil {
|
||||
outs = append(outs, stdOut)
|
||||
}
|
||||
cmd.SetOut(io.MultiWriter(outs...))
|
||||
errs := []io.Writer{debugStderr}
|
||||
if stdErr != nil {
|
||||
errs = append(errs, stdErr)
|
||||
}
|
||||
cmd.SetErr(io.MultiWriter(errs...))
|
||||
|
||||
if args == nil {
|
||||
args = []string{}
|
||||
}
|
||||
cmd.SetArgs(args)
|
||||
}
|
||||
|
||||
// ExecBackgroundCtx runs the cobra command in the background.
|
||||
func ExecBackgroundCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group {
|
||||
prepareCmd(cmd, stdIn, stdOut, stdErr, args)
|
||||
|
||||
eg := &errgroup.Group{}
|
||||
eg.Go(func() error {
|
||||
defer cmd.SetIn(nil)
|
||||
return cmd.ExecuteContext(ctx)
|
||||
})
|
||||
|
||||
return eg
|
||||
}
|
||||
|
||||
// Exec runs the provided cobra command with the given reader as STD_IN and the given args.
|
||||
// Returns STD_OUT, STD_ERR and the error from the execution.
|
||||
func Exec(t testing.TB, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) {
|
||||
return ExecCtx(t.Context(), cmd, stdIn, args...)
|
||||
}
|
||||
|
||||
func ExecCtx(ctx context.Context, cmd *cobra.Command, stdIn io.Reader, args ...string) (string, string, error) {
|
||||
stdOut, stdErr := &bytes.Buffer{}, &bytes.Buffer{}
|
||||
|
||||
prepareCmd(cmd, stdIn, stdOut, stdErr, args)
|
||||
|
||||
// needs to be on a separate line to ensure that the output buffers are read AFTER the command ran
|
||||
err := cmd.ExecuteContext(ctx)
|
||||
|
||||
return stdOut.String(), stdErr.String(), err
|
||||
}
|
||||
|
||||
// ExecNoErr is a helper that assumes a successful run from Exec.
|
||||
// Returns STD_OUT.
|
||||
func ExecNoErr(t testing.TB, cmd *cobra.Command, args ...string) string {
|
||||
return ExecNoErrCtx(t.Context(), t, cmd, args...)
|
||||
}
|
||||
|
||||
func ExecNoErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string {
|
||||
stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...)
|
||||
if err == nil {
|
||||
require.Len(t, stdErr, 0, "std_out: %s\nstd_err: %s", stdOut, stdErr)
|
||||
} else {
|
||||
require.ErrorIsf(t, err, context.Canceled, "std_out: %s\nstd_err: %s", stdOut, stdErr)
|
||||
}
|
||||
return stdOut
|
||||
}
|
||||
|
||||
// ExecExpectedErr is a helper that assumes a failing run from Exec returning ErrNoPrintButFail
|
||||
// Returns STD_ERR.
|
||||
func ExecExpectedErr(t testing.TB, cmd *cobra.Command, args ...string) string {
|
||||
return ExecExpectedErrCtx(t.Context(), t, cmd, args...)
|
||||
}
|
||||
|
||||
func ExecExpectedErrCtx(ctx context.Context, t require.TestingT, cmd *cobra.Command, args ...string) string {
|
||||
stdOut, stdErr, err := ExecCtx(ctx, cmd, nil, args...)
|
||||
require.True(t, errors.Is(err, ErrNoPrintButFail), "std_out: %s\nstd_err: %s", stdOut, stdErr)
|
||||
require.Len(t, stdOut, 0, stdErr)
|
||||
return stdErr
|
||||
}
|
||||
|
||||
type CommandExecuter struct {
|
||||
New func() *cobra.Command
|
||||
Ctx context.Context
|
||||
PersistentArgs []string
|
||||
}
|
||||
|
||||
func (c *CommandExecuter) Exec(stdin io.Reader, args ...string) (string, string, error) {
|
||||
return ExecCtx(c.Ctx, c.New(), stdin, append(c.PersistentArgs, args...)...)
|
||||
}
|
||||
|
||||
func (c *CommandExecuter) ExecBackground(stdin io.Reader, stdOut, stdErr io.Writer, args ...string) *errgroup.Group {
|
||||
return ExecBackgroundCtx(c.Ctx, c.New(), stdin, stdOut, stdErr, append(c.PersistentArgs, args...)...)
|
||||
}
|
||||
|
||||
func (c *CommandExecuter) ExecNoErr(t require.TestingT, args ...string) string {
|
||||
return ExecNoErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...)
|
||||
}
|
||||
|
||||
func (c *CommandExecuter) ExecExpectedErr(t require.TestingT, args ...string) string {
|
||||
return ExecExpectedErrCtx(c.Ctx, t, c.New(), append(c.PersistentArgs, args...)...)
|
||||
}
|
||||
|
||||
type URL struct {
|
||||
url.URL
|
||||
}
|
||||
|
||||
var _ pflag.Value = (*URL)(nil)
|
||||
|
||||
func (u *URL) Set(s string) error {
|
||||
uu, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.URL = *uu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*URL) Type() string {
|
||||
return "url"
|
||||
}
|
||||
125
oryx/cmdx/http.go
Normal file
125
oryx/cmdx/http.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/ory/x/httpx"
|
||||
)
|
||||
|
||||
const (
|
||||
envKeyEndpoint = "ORY_SDK_URL"
|
||||
FlagEndpoint = "endpoint"
|
||||
FlagSkipTLSVerify = "skip-tls-verify"
|
||||
FlagHeaders = "http-header"
|
||||
)
|
||||
|
||||
// Remote returns the remote endpoint for the given command.
|
||||
func Remote(cmd *cobra.Command) (string, error) {
|
||||
endpoint, err := cmd.Flags().GetString(FlagEndpoint)
|
||||
if err != nil {
|
||||
return "", errors.WithStack(err)
|
||||
}
|
||||
|
||||
if endpoint != "" {
|
||||
return strings.TrimRight(endpoint, "/"), nil
|
||||
} else if endpoint := os.Getenv("ORY_SDK_URL"); endpoint != "" {
|
||||
return strings.TrimRight(endpoint, "/"), nil
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "To execute this command, the endpoint URL must point to the URL where Ory is located. To set the endpoint URL, use flag `--endpoint` or environment variable `ORY_SDK_URL`.")
|
||||
return "", FailSilently(cmd)
|
||||
}
|
||||
|
||||
// RemoteURI returns the remote URI for the given command.
|
||||
func RemoteURI(cmd *cobra.Command) (*url.URL, error) {
|
||||
remote, err := Remote(cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoint, err := url.ParseRequestURI(remote)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse endpoint URL: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
// NewClient creates a new HTTP client.
|
||||
func NewClient(cmd *cobra.Command) (*http.Client, *url.URL, error) {
|
||||
endpoint, err := cmd.Flags().GetString(FlagEndpoint)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
endpoint = os.Getenv(envKeyEndpoint)
|
||||
}
|
||||
|
||||
if endpoint == "" {
|
||||
return nil, nil, errors.Errorf("you have to set the remote endpoint, try --help for details")
|
||||
}
|
||||
|
||||
u, err := url.Parse(strings.TrimRight(endpoint, "/"))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, `could not parse the endpoint URL "%s"`, endpoint)
|
||||
}
|
||||
|
||||
hc := retryablehttp.NewClient().StandardClient()
|
||||
hc.Timeout = time.Second * 10
|
||||
|
||||
rawHeaders, err := cmd.Flags().GetStringSlice(FlagHeaders)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
header := http.Header{}
|
||||
for _, h := range rawHeaders {
|
||||
parts := strings.Split(h, ":")
|
||||
if len(parts) != 2 {
|
||||
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Unable to parse `--http-header` flag. Format of flag value is a `: ` delimited string like `--http-header 'Some-Header: some-values; other values`. Received: %v", rawHeaders)
|
||||
return nil, nil, FailSilently(cmd)
|
||||
}
|
||||
|
||||
for k := range parts {
|
||||
parts[k] = strings.TrimSpace(parts[k])
|
||||
}
|
||||
|
||||
header.Add(parts[0], parts[1])
|
||||
}
|
||||
|
||||
skipVerify, err := cmd.Flags().GetBool(FlagSkipTLSVerify)
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
rt := httpx.NewTransportWithHeader(header)
|
||||
rt.RoundTripper = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: skipVerify, //nolint:gosec // This is a false positive
|
||||
},
|
||||
}
|
||||
hc.Transport = rt
|
||||
return hc, u, nil
|
||||
}
|
||||
|
||||
// RegisterHTTPClientFlags registers HTTP client configuration flags.
|
||||
func RegisterHTTPClientFlags(flags *pflag.FlagSet) {
|
||||
flags.StringP(FlagEndpoint, FlagEndpoint[:1], "", fmt.Sprintf("The API URL this command should target. Alternatively set using the %s environmental variable.", envKeyEndpoint))
|
||||
flags.Bool(FlagSkipTLSVerify, false, "Do not verify TLS certificates. Useful when dealing with self-signed certificates. Do not use in production!")
|
||||
flags.StringSliceP(FlagHeaders, "H", []string{}, "A list of additional HTTP headers to set. HTTP headers is separated by a `: `, for example: `-H 'Authorization: bearer some-token'`.")
|
||||
}
|
||||
137
oryx/cmdx/noise_printer.go
Normal file
137
oryx/cmdx/noise_printer.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type ConditionalPrinter struct {
|
||||
w io.Writer
|
||||
print bool
|
||||
}
|
||||
|
||||
const (
|
||||
FlagQuiet = "quiet"
|
||||
)
|
||||
|
||||
func RegisterNoiseFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolP(FlagQuiet, FlagQuiet[:1], false, "Be quiet with output printing.")
|
||||
}
|
||||
|
||||
// NewLoudOutPrinter returns a ConditionalPrinter that
|
||||
// only prints to cmd.OutOrStdout when --quiet is not set
|
||||
func NewLoudOutPrinter(cmd *cobra.Command) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: cmd.OutOrStdout(),
|
||||
print: !quiet,
|
||||
}
|
||||
}
|
||||
|
||||
// NewQuietOutPrinter returns a ConditionalPrinter that
|
||||
// only prints to cmd.OutOrStdout when --quiet is set
|
||||
func NewQuietOutPrinter(cmd *cobra.Command) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: cmd.OutOrStdout(),
|
||||
print: quiet,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLoudErrPrinter returns a ConditionalPrinter that
|
||||
// only prints to cmd.ErrOrStderr when --quiet is not set
|
||||
func NewLoudErrPrinter(cmd *cobra.Command) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: cmd.ErrOrStderr(),
|
||||
print: !quiet,
|
||||
}
|
||||
}
|
||||
|
||||
// NewQuietErrPrinter returns a ConditionalPrinter that
|
||||
// only prints to cmd.ErrOrStderr when --quiet is set
|
||||
func NewQuietErrPrinter(cmd *cobra.Command) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: cmd.ErrOrStderr(),
|
||||
print: quiet,
|
||||
}
|
||||
}
|
||||
|
||||
// NewLoudPrinter returns a ConditionalPrinter that
|
||||
// only prints to w when --quiet is not set
|
||||
func NewLoudPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: w,
|
||||
print: !quiet,
|
||||
}
|
||||
}
|
||||
|
||||
// NewQuietPrinter returns a ConditionalPrinter that
|
||||
// only prints to w when --quiet is set
|
||||
func NewQuietPrinter(cmd *cobra.Command, w io.Writer) *ConditionalPrinter {
|
||||
quiet, err := cmd.Flags().GetBool(FlagQuiet)
|
||||
if err != nil {
|
||||
Fatalf(err.Error())
|
||||
}
|
||||
|
||||
return &ConditionalPrinter{
|
||||
w: w,
|
||||
print: quiet,
|
||||
}
|
||||
}
|
||||
|
||||
func NewConditionalPrinter(w io.Writer, print bool) *ConditionalPrinter {
|
||||
return &ConditionalPrinter{
|
||||
w: w,
|
||||
print: print,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ConditionalPrinter) Println(a ...interface{}) (n int, err error) {
|
||||
if p.print {
|
||||
return fmt.Fprintln(p.w, a...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ConditionalPrinter) Print(a ...interface{}) (n int, err error) {
|
||||
if p.print {
|
||||
return fmt.Fprint(p.w, a...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *ConditionalPrinter) Printf(format string, a ...interface{}) (n int, err error) {
|
||||
if p.print {
|
||||
return fmt.Fprintf(p.w, format, a...)
|
||||
}
|
||||
return
|
||||
}
|
||||
84
oryx/cmdx/output.go
Normal file
84
oryx/cmdx/output.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import "strconv"
|
||||
|
||||
type (
|
||||
// OutputIder outputs an ID
|
||||
OutputIder string
|
||||
// OutputIderCollection outputs a list of IDs
|
||||
OutputIderCollection struct {
|
||||
Items []OutputIder
|
||||
}
|
||||
)
|
||||
|
||||
func (OutputIder) Header() []string {
|
||||
return []string{"ID"}
|
||||
}
|
||||
|
||||
func (i OutputIder) Columns() []string {
|
||||
return []string{string(i)}
|
||||
}
|
||||
|
||||
func (i OutputIder) Interface() interface{} {
|
||||
return i
|
||||
}
|
||||
|
||||
func (OutputIderCollection) Header() []string {
|
||||
return []string{"ID"}
|
||||
}
|
||||
|
||||
func (c OutputIderCollection) Table() [][]string {
|
||||
rows := make([][]string, len(c.Items))
|
||||
for i, ident := range c.Items {
|
||||
rows[i] = []string{string(ident)}
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func (c OutputIderCollection) Interface() interface{} {
|
||||
return c.Items
|
||||
}
|
||||
|
||||
func (c OutputIderCollection) Len() int {
|
||||
return len(c.Items)
|
||||
}
|
||||
|
||||
type PaginatedList struct {
|
||||
Collection interface {
|
||||
Table
|
||||
IDs() []string
|
||||
} `json:"-"`
|
||||
Items []interface{} `json:"items"`
|
||||
NextPageToken string `json:"next_page_token"`
|
||||
IsLastPage bool `json:"is_last_page"`
|
||||
}
|
||||
|
||||
func (r *PaginatedList) Header() []string {
|
||||
return r.Collection.Header()
|
||||
}
|
||||
|
||||
func (r *PaginatedList) Table() [][]string {
|
||||
return append(
|
||||
r.Collection.Table(),
|
||||
[]string{},
|
||||
[]string{"NEXT PAGE TOKEN", r.NextPageToken},
|
||||
[]string{"IS LAST PAGE", strconv.FormatBool(r.IsLastPage)},
|
||||
)
|
||||
}
|
||||
|
||||
func (r *PaginatedList) Interface() interface{} {
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *PaginatedList) Len() int {
|
||||
return r.Collection.Len() + 3
|
||||
}
|
||||
|
||||
func (r *PaginatedList) IDs() []string {
|
||||
return r.Collection.IDs()
|
||||
}
|
||||
|
||||
var _ Table = (*PaginatedList)(nil)
|
||||
57
oryx/cmdx/pagination.go
Normal file
57
oryx/cmdx/pagination.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
FlagPageSize = "page-size"
|
||||
FlagPageToken = "page-token"
|
||||
)
|
||||
|
||||
func RegisterTokenPaginationFlags(cmd *cobra.Command) (pageSize int, pageToken string) {
|
||||
cmd.Flags().StringVar(&pageToken, FlagPageToken, "", "page token acquired from a previous response")
|
||||
cmd.Flags().IntVar(&pageSize, FlagPageSize, 100, "maximum number of items to return")
|
||||
return
|
||||
}
|
||||
|
||||
// ParsePaginationArgs parses pagination arguments from the command line.
|
||||
func ParsePaginationArgs(cmd *cobra.Command, pageArg, perPageArg string) (page, perPage int64, err error) {
|
||||
if len(pageArg+perPageArg) > 0 {
|
||||
page, err = strconv.ParseInt(pageArg, 0, 64)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse page argument\"%s\": %s", pageArg, err)
|
||||
return 0, 0, FailSilently(cmd)
|
||||
}
|
||||
|
||||
perPage, err = strconv.ParseInt(perPageArg, 0, 64)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse per-page argument\"%s\": %s", perPageArg, err)
|
||||
return 0, 0, FailSilently(cmd)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ParseTokenPaginationArgs parses token-based pagination arguments from the command line.
|
||||
func ParseTokenPaginationArgs(cmd *cobra.Command) (page string, perPage int, err error) {
|
||||
pageArg, err := cmd.Flags().GetString(FlagPageToken)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%s\": %s", FlagPageToken, pageArg, err)
|
||||
return "", 0, FailSilently(cmd)
|
||||
}
|
||||
|
||||
perPageArg, err := cmd.Flags().GetInt(FlagPageSize)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Could not parse %s argument \"%d\": %s", FlagPageSize, perPageArg, err)
|
||||
return "", 0, FailSilently(cmd)
|
||||
}
|
||||
|
||||
return pageArg, perPageArg, nil
|
||||
}
|
||||
355
oryx/cmdx/printing.go
Normal file
355
oryx/cmdx/printing.go
Normal file
@@ -0,0 +1,355 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/go-openapi/jsonpointer"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type (
|
||||
TableHeader interface {
|
||||
Header() []string
|
||||
}
|
||||
TableRow interface {
|
||||
TableHeader
|
||||
Columns() []string
|
||||
Interface() interface{}
|
||||
}
|
||||
Table interface {
|
||||
TableHeader
|
||||
Table() [][]string
|
||||
Interface() interface{}
|
||||
Len() int
|
||||
}
|
||||
Nil struct{}
|
||||
|
||||
Format string
|
||||
)
|
||||
|
||||
const (
|
||||
FormatQuiet Format = "quiet"
|
||||
FormatTable Format = "table"
|
||||
FormatJSON Format = "json"
|
||||
FormatJSONPath Format = "jsonpath"
|
||||
FormatJSONPointer Format = "jsonpointer"
|
||||
FormatJSONPretty Format = "json-pretty"
|
||||
FormatYAML Format = "yaml"
|
||||
FormatDefault Format = "default"
|
||||
|
||||
FlagFormat = "format"
|
||||
|
||||
None = "<none>"
|
||||
)
|
||||
|
||||
func (Nil) String() string {
|
||||
return "null"
|
||||
}
|
||||
|
||||
func (Nil) Interface() interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
type printOptions struct {
|
||||
format string
|
||||
}
|
||||
|
||||
type PrintOption func(*printOptions)
|
||||
|
||||
func WithFormat(v string) PrintOption {
|
||||
return func(o *printOptions) {
|
||||
o.format = v
|
||||
}
|
||||
}
|
||||
|
||||
func PrintErrors(cmd *cobra.Command, errs map[string]error) {
|
||||
for src, err := range errs {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s: %s\n", src, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func PrintRow(cmd *cobra.Command, row TableRow) {
|
||||
PrintRowf(cmd.OutOrStdout(), row, WithFormat(getFormatValue(cmd)))
|
||||
}
|
||||
|
||||
func PrintRowf(w io.Writer, row TableRow, opts ...PrintOption) {
|
||||
o := &printOptions{}
|
||||
for _, fn := range opts {
|
||||
fn(o)
|
||||
}
|
||||
|
||||
switch parseFormat(o.format) {
|
||||
case FormatQuiet:
|
||||
if idAble, ok := row.(interface{ ID() string }); ok {
|
||||
_, _ = fmt.Fprintln(w, idAble.ID())
|
||||
break
|
||||
}
|
||||
_, _ = fmt.Fprintln(w, row.Columns()[0])
|
||||
case FormatJSON:
|
||||
printJSON(w, row.Interface(), false, "")
|
||||
case FormatYAML:
|
||||
printYAML(w, row.Interface())
|
||||
case FormatJSONPretty:
|
||||
printJSON(w, row.Interface(), true, "")
|
||||
case FormatJSONPath:
|
||||
printJSON(w, row.Interface(), true, getPath(o.format))
|
||||
case FormatJSONPointer:
|
||||
printJSON(w, filterJSONPointer(o.format, row.Interface()), true, "")
|
||||
case FormatTable, FormatDefault:
|
||||
w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0)
|
||||
|
||||
fields := row.Columns()
|
||||
for i, h := range row.Header() {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t\n", h, fields[i])
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func filterJSONPointer(f string, data any) any {
|
||||
_, jsonptr, found := strings.Cut(f, "=")
|
||||
if !found {
|
||||
_, _ = fmt.Fprintf(os.Stderr,
|
||||
"Format %s is missing a JSON pointer, e.g., --%s=%s=<jsonpointer>. The path syntax is described at https://datatracker.ietf.org/doc/html/draft-ietf-appsawg-json-pointer-07.",
|
||||
f, FlagFormat, f)
|
||||
os.Exit(1)
|
||||
}
|
||||
ptr, err := jsonpointer.New(jsonptr)
|
||||
Must(err, "invalid JSON pointer: %s", err)
|
||||
|
||||
result, _, err := ptr.Get(data)
|
||||
Must(err, "failed to apply JSON pointer: %s", err)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func PrintTable(cmd *cobra.Command, table Table) {
|
||||
PrintTablef(cmd.OutOrStdout(), table, WithFormat(getFormatValue(cmd)))
|
||||
}
|
||||
|
||||
func PrintTablef(w io.Writer, table Table, opts ...PrintOption) {
|
||||
o := &printOptions{}
|
||||
for _, fn := range opts {
|
||||
fn(o)
|
||||
}
|
||||
|
||||
switch parseFormat(o.format) {
|
||||
case FormatQuiet:
|
||||
if table.Len() == 0 {
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
|
||||
if idAble, ok := table.(interface{ IDs() []string }); ok {
|
||||
for _, row := range idAble.IDs() {
|
||||
fmt.Fprintln(w, row)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
for _, row := range table.Table() {
|
||||
fmt.Fprintln(w, row[0])
|
||||
}
|
||||
case FormatJSON:
|
||||
printJSON(w, table.Interface(), false, "")
|
||||
case FormatJSONPretty:
|
||||
printJSON(w, table.Interface(), true, "")
|
||||
case FormatJSONPath:
|
||||
printJSON(w, table.Interface(), true, getPath(o.format))
|
||||
case FormatJSONPointer:
|
||||
printJSON(w, filterJSONPointer(o.format, table.Interface()), true, "")
|
||||
case FormatYAML:
|
||||
printYAML(w, table.Interface())
|
||||
default:
|
||||
w := tabwriter.NewWriter(w, 0, 8, 1, '\t', 0)
|
||||
|
||||
for _, h := range table.Header() {
|
||||
fmt.Fprintf(w, "%s\t", h)
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
|
||||
for _, row := range table.Table() {
|
||||
fmt.Fprintln(w, strings.Join(row, "\t")+"\t")
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
type interfacer interface{ Interface() interface{} }
|
||||
|
||||
func PrintJSONAble(cmd *cobra.Command, d interface{ String() string }) {
|
||||
PrintJSONAblef(cmd.OutOrStdout(), d, WithFormat(getFormatValue(cmd)))
|
||||
}
|
||||
|
||||
func PrintJSONAblef(w io.Writer, d interface{ String() string }, opts ...PrintOption) {
|
||||
if d == nil {
|
||||
d = Nil{}
|
||||
}
|
||||
|
||||
o := &printOptions{}
|
||||
for _, fn := range opts {
|
||||
fn(o)
|
||||
}
|
||||
|
||||
var path string
|
||||
switch parseFormat(o.format) {
|
||||
default:
|
||||
_, _ = fmt.Fprint(w, d.String())
|
||||
case FormatJSON:
|
||||
var v interface{} = d
|
||||
if i, ok := d.(interfacer); ok {
|
||||
v = i
|
||||
}
|
||||
printJSON(w, v, false, "")
|
||||
case FormatJSONPath:
|
||||
path = getPath(o.format)
|
||||
fallthrough
|
||||
case FormatJSONPretty:
|
||||
var v interface{} = d
|
||||
if i, ok := d.(interfacer); ok {
|
||||
v = i
|
||||
}
|
||||
printJSON(w, v, true, path)
|
||||
case FormatJSONPointer:
|
||||
var v interface{} = d
|
||||
if i, ok := d.(interfacer); ok {
|
||||
v = i
|
||||
}
|
||||
printJSON(w, filterJSONPointer(o.format, v), true, "")
|
||||
case FormatYAML:
|
||||
var v interface{} = d
|
||||
if i, ok := d.(interfacer); ok {
|
||||
v = i
|
||||
}
|
||||
printYAML(w, v)
|
||||
}
|
||||
}
|
||||
|
||||
func getQuiet(cmd *cobra.Command) bool {
|
||||
// ignore the error here as we use this function also when the flag might not be registered
|
||||
q, _ := cmd.Flags().GetBool(FlagQuiet)
|
||||
return q
|
||||
}
|
||||
|
||||
func getFormatValue(cmd *cobra.Command) string {
|
||||
if getQuiet(cmd) {
|
||||
return string(FormatQuiet)
|
||||
}
|
||||
|
||||
f, _ := cmd.Flags().GetString(FlagFormat)
|
||||
return f
|
||||
}
|
||||
|
||||
func parseFormat(f string) Format {
|
||||
switch {
|
||||
case f == string(FormatQuiet):
|
||||
return FormatQuiet
|
||||
case f == string(FormatTable):
|
||||
return FormatTable
|
||||
case f == string(FormatJSON):
|
||||
return FormatJSON
|
||||
case strings.HasPrefix(f, string(FormatJSONPath)):
|
||||
return FormatJSONPath
|
||||
case strings.HasPrefix(f, string(FormatJSONPointer)):
|
||||
return FormatJSONPointer
|
||||
case f == string(FormatJSONPretty):
|
||||
return FormatJSONPretty
|
||||
case f == string(FormatYAML):
|
||||
return FormatYAML
|
||||
default:
|
||||
return FormatDefault
|
||||
}
|
||||
}
|
||||
|
||||
func getPath(f string) string {
|
||||
_, path, found := strings.Cut(f, "=")
|
||||
if !found {
|
||||
_, _ = fmt.Fprintf(os.Stderr,
|
||||
"Format %s is missing a path, e.g., --%s=%s=<path>. The path syntax is described at https://github.com/tidwall/gjson/blob/master/SYNTAX.md",
|
||||
f, FlagFormat, f)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func printJSON(w io.Writer, v interface{}, pretty bool, path string) {
|
||||
if path != "" {
|
||||
temp, err := json.Marshal(v)
|
||||
Must(err, "Error encoding JSON: %s", err)
|
||||
v = gjson.GetBytes(temp, path).Value()
|
||||
}
|
||||
|
||||
e := json.NewEncoder(w)
|
||||
if pretty {
|
||||
e.SetIndent("", " ")
|
||||
}
|
||||
err := e.Encode(v)
|
||||
// unexpected error
|
||||
Must(err, "Error encoding JSON: %s", err)
|
||||
}
|
||||
|
||||
func printYAML(w io.Writer, v interface{}) {
|
||||
j, err := json.Marshal(v)
|
||||
Must(err, "Error encoding JSON: %s", err)
|
||||
e, err := yaml.JSONToYAML(j)
|
||||
Must(err, "Error encoding YAML: %s", err)
|
||||
_, _ = w.Write(e)
|
||||
}
|
||||
|
||||
func RegisterJSONFormatFlags(flags *pflag.FlagSet) {
|
||||
flags.String(FlagFormat, string(FormatDefault), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatDefault, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer))
|
||||
}
|
||||
|
||||
func RegisterFormatFlags(flags *pflag.FlagSet) {
|
||||
RegisterNoiseFlags(flags)
|
||||
flags.String(FlagFormat, string(FormatTable), fmt.Sprintf("Set the output format. One of %s, %s, %s, %s, %s and %s.", FormatTable, FormatJSON, FormatYAML, FormatJSONPretty, FormatJSONPath, FormatJSONPointer))
|
||||
}
|
||||
|
||||
func PrintOpenAPIError(cmd *cobra.Command, err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var be interface {
|
||||
Body() []byte
|
||||
}
|
||||
if !errors.As(err, &be) {
|
||||
return err
|
||||
}
|
||||
|
||||
body := be.Body()
|
||||
didPrettyPrint := false
|
||||
if message := gjson.GetBytes(body, "error.message"); message.Exists() {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", message.String())
|
||||
didPrettyPrint = true
|
||||
}
|
||||
if reason := gjson.GetBytes(body, "error.reason"); reason.Exists() {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", reason.String())
|
||||
didPrettyPrint = true
|
||||
}
|
||||
|
||||
if didPrettyPrint {
|
||||
return FailSilently(cmd)
|
||||
}
|
||||
|
||||
if body, err := json.MarshalIndent(json.RawMessage(body), "", " "); err == nil {
|
||||
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\nFailed to execute API request, see error above.\n", body)
|
||||
return FailSilently(cmd)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
122
oryx/cmdx/usage.go
Normal file
122
oryx/cmdx/usage.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"maps"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
usageTemplateFuncs = sprig.TxtFuncMap()
|
||||
usageTemplateFuncsMux sync.RWMutex
|
||||
enableUsageTemplatingOnce sync.Once
|
||||
)
|
||||
|
||||
// AddUsageTemplateFunc adds a template function to the usage template.
|
||||
func AddUsageTemplateFunc(name string, f interface{}) {
|
||||
usageTemplateFuncsMux.Lock()
|
||||
usageTemplateFuncs[name] = f
|
||||
usageTemplateFuncsMux.Unlock()
|
||||
}
|
||||
|
||||
const (
|
||||
helpTemplate = `{{insertTemplate . (or .Long .Short) | trimTrailingWhitespaces}}
|
||||
|
||||
{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}`
|
||||
usageTemplate = `Usage:{{if .Runnable}}
|
||||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
|
||||
|
||||
Aliases:
|
||||
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
||||
|
||||
Examples:
|
||||
{{insertTemplate . .Example}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
||||
`
|
||||
)
|
||||
|
||||
// EnableUsageTemplating enables gotemplates for usage strings, i.e. cmd.Short, cmd.Long, and cmd.Example.
|
||||
// The data for the template is the command itself. Especially useful are `.Root.Name` and `.CommandPath`.
|
||||
// This will be inherited by all subcommands, so enabling it on the root command is sufficient.
|
||||
func EnableUsageTemplating(cmds ...*cobra.Command) {
|
||||
enableUsageTemplatingOnce.Do(func() {
|
||||
cobra.AddTemplateFunc("insertTemplate", TemplateCommandField)
|
||||
})
|
||||
for _, cmd := range cmds {
|
||||
cmd.SetHelpTemplate(helpTemplate)
|
||||
cmd.SetUsageTemplate(usageTemplate)
|
||||
}
|
||||
}
|
||||
|
||||
func TemplateCommandField(cmd *cobra.Command, field string) (string, error) {
|
||||
t := template.New("")
|
||||
usageTemplateFuncsMux.RLock()
|
||||
t.Funcs(maps.Clone(usageTemplateFuncs))
|
||||
usageTemplateFuncsMux.RUnlock()
|
||||
t, err := t.Parse(field)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := t.Execute(&out, cmd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
|
||||
// DisableUsageTemplating resets the commands usage template to the default.
|
||||
// This can be used to undo the effects of EnableUsageTemplating, specifically for a subcommand.
|
||||
func DisableUsageTemplating(cmds ...*cobra.Command) {
|
||||
defaultCmd := new(cobra.Command)
|
||||
for _, cmd := range cmds {
|
||||
cmd.SetHelpTemplate(defaultCmd.HelpTemplate())
|
||||
cmd.SetUsageTemplate(defaultCmd.UsageTemplate())
|
||||
}
|
||||
}
|
||||
|
||||
// AssertUsageTemplates asserts that the usage string of the commands are properly templated.
|
||||
func AssertUsageTemplates(t require.TestingT, cmd *cobra.Command) {
|
||||
var usage, help string
|
||||
require.NotPanics(t, func() {
|
||||
usage = cmd.UsageString()
|
||||
|
||||
out, err := cmd.OutOrStdout(), cmd.ErrOrStderr()
|
||||
bb := new(bytes.Buffer)
|
||||
|
||||
cmd.SetOut(bb)
|
||||
cmd.SetErr(bb)
|
||||
require.NoError(t, cmd.Help())
|
||||
help = bb.String()
|
||||
|
||||
cmd.SetOut(out)
|
||||
cmd.SetErr(err)
|
||||
})
|
||||
assert.NotContains(t, usage, "{{")
|
||||
assert.NotContains(t, help, "{{")
|
||||
for _, child := range cmd.Commands() {
|
||||
AssertUsageTemplates(t, child)
|
||||
}
|
||||
}
|
||||
57
oryx/cmdx/user_input.go
Normal file
57
oryx/cmdx/user_input.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// asks for confirmation with the question string s and reads the answer
|
||||
// pass nil to use os.Stdin and os.Stdout
|
||||
func AskForConfirmation(s string, stdin io.Reader, stdout io.Writer) bool {
|
||||
if stdin == nil {
|
||||
stdin = os.Stdin
|
||||
}
|
||||
if stdout == nil {
|
||||
stdout = os.Stdout
|
||||
}
|
||||
|
||||
ok, err := AskScannerForConfirmation(s, bufio.NewReader(stdin), stdout)
|
||||
if err != nil {
|
||||
Must(err, "Unable to confirm: %s", err)
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func AskScannerForConfirmation(s string, reader *bufio.Reader, stdout io.Writer) (bool, error) {
|
||||
if stdout == nil {
|
||||
stdout = os.Stdout
|
||||
}
|
||||
|
||||
for {
|
||||
_, err := fmt.Fprintf(stdout, "%s [y/n]: ", s)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to print to stdout")
|
||||
}
|
||||
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "unable to read from stdin")
|
||||
}
|
||||
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
if response == "y" || response == "yes" {
|
||||
return true, nil
|
||||
} else if response == "n" || response == "no" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
38
oryx/cmdx/version.go
Normal file
38
oryx/cmdx/version.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cmdx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Version returns a *cobra.Command that handles the `version` command.
|
||||
func Version(gitTag, gitHash, buildTime *string) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show the build version, build time, and git hash",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(*gitTag) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Unable to determine version because the build process did not properly configure it.")
|
||||
} else {
|
||||
fmt.Printf("Version: %s\n", *gitTag)
|
||||
}
|
||||
|
||||
if len(*gitHash) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Unable to determine build commit because the build process did not properly configure it.")
|
||||
} else {
|
||||
fmt.Printf("Build Commit: %s\n", *gitHash)
|
||||
}
|
||||
|
||||
if len(*buildTime) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Unable to determine build timestamp because the build process did not properly configure it.")
|
||||
} else {
|
||||
fmt.Printf("Build Timestamp: %s\n", *buildTime)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
1
oryx/configx/.snapshots/TestKoanfSchemaDefaults.json
Normal file
1
oryx/configx/.snapshots/TestKoanfSchemaDefaults.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
22
oryx/configx/context.go
Normal file
22
oryx/configx/context.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import "context"
|
||||
|
||||
type contextKey int
|
||||
|
||||
const configContextKey contextKey = iota + 1
|
||||
|
||||
func ContextWithConfigOptions(ctx context.Context, opts ...OptionModifier) context.Context {
|
||||
return context.WithValue(ctx, configContextKey, opts)
|
||||
}
|
||||
|
||||
func ConfigOptionsFromContext(ctx context.Context) []OptionModifier {
|
||||
opts, ok := ctx.Value(configContextKey).([]OptionModifier)
|
||||
if !ok {
|
||||
return []OptionModifier{}
|
||||
}
|
||||
return opts
|
||||
}
|
||||
41
oryx/configx/cors.go
Normal file
41
oryx/configx/cors.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/cors"
|
||||
|
||||
"github.com/ory/x/corsx"
|
||||
)
|
||||
|
||||
const CORSConfigSchemaID = "ory://cors-config"
|
||||
|
||||
//go:embed cors.schema.json
|
||||
var CORSConfigSchema []byte
|
||||
|
||||
func (p *Provider) CORS(prefix string, defaults cors.Options) (cors.Options, bool) {
|
||||
prefix = cleanPrefix(prefix)
|
||||
allowedOrigins := p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins)
|
||||
allowInsecureOrigins := p.BoolF("feature_flags.legacy_allow_insecure_origins", false)
|
||||
return cors.Options{
|
||||
// Populated even though rs/cors ignores it once AllowOriginVaryRequestFunc
|
||||
// is set: some consumers read AllowedOrigins directly — notably hydra's
|
||||
// oauth2cors.Middleware (which builds its own matcher and treats an empty
|
||||
// list as "allow all", a CORS bypass) and oathkeeper's address detection.
|
||||
AllowedOrigins: p.StringsF(prefix+"cors.allowed_origins", defaults.AllowedOrigins),
|
||||
AllowedMethods: p.StringsF(prefix+"cors.allowed_methods", defaults.AllowedMethods),
|
||||
AllowedHeaders: p.StringsF(prefix+"cors.allowed_headers", defaults.AllowedHeaders),
|
||||
ExposedHeaders: p.StringsF(prefix+"cors.exposed_headers", defaults.ExposedHeaders),
|
||||
AllowCredentials: p.BoolF(prefix+"cors.allow_credentials", defaults.AllowCredentials),
|
||||
OptionsPassthrough: p.BoolF(prefix+"cors.options_passthrough", defaults.OptionsPassthrough),
|
||||
MaxAge: p.IntF(prefix+"cors.max_age", defaults.MaxAge),
|
||||
Debug: p.BoolF(prefix+"cors.debug", defaults.Debug),
|
||||
AllowOriginVaryRequestFunc: func(_ *http.Request, origin string) (bool, []string) {
|
||||
return corsx.CheckOrigin(allowedOrigins, origin, allowInsecureOrigins), nil
|
||||
},
|
||||
}, p.Bool(prefix + "cors.enabled")
|
||||
}
|
||||
106
oryx/configx/cors.schema.json
Normal file
106
oryx/configx/cors.schema.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"$id": "https://github.com/ory/x/configx/cors.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"title": "CORS",
|
||||
"description": "Configures Cross Origin Resource Sharing for this endpoint.",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"allowed_origins": {
|
||||
"type": "array",
|
||||
"description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: https://*.example.com). Only one wildcard can be used per origin.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"not": {
|
||||
"type": "string",
|
||||
"description": "matches all strings that contain two or more (*)",
|
||||
"pattern": ".*\\*.*\\*.*"
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
},
|
||||
{
|
||||
"const": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"examples": [
|
||||
[
|
||||
"https://example.com",
|
||||
"https://*.example.com",
|
||||
"https://*.foo.example.com"
|
||||
]
|
||||
]
|
||||
},
|
||||
"allowed_methods": {
|
||||
"type": "array",
|
||||
"description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"POST",
|
||||
"GET",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"TRACE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allowed_headers": {
|
||||
"type": "array",
|
||||
"description": "A list of non-simple headers the client is allowed to use with cross-domain requests.",
|
||||
"examples": [
|
||||
[
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"Max-Age",
|
||||
"X-Session-Token",
|
||||
"X-XSRF-TOKEN",
|
||||
"X-CSRF-TOKEN"
|
||||
]
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"exposed_headers": {
|
||||
"type": "array",
|
||||
"description": "Sets which headers are safe to expose to the API of a CORS API specification.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"allow_credentials": {
|
||||
"type": "boolean",
|
||||
"description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.",
|
||||
"default": true
|
||||
},
|
||||
"options_passthrough": {
|
||||
"type": "boolean",
|
||||
"description": "TODO",
|
||||
"default": false
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.",
|
||||
"default": 0,
|
||||
"minimum": 0
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "Adds additional log output to debug CORS issues.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
30
oryx/configx/error.go
Normal file
30
oryx/configx/error.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ImmutableError struct {
|
||||
From interface{}
|
||||
To interface{}
|
||||
Key string
|
||||
error
|
||||
}
|
||||
|
||||
func NewImmutableError(key string, from, to interface{}) error {
|
||||
return &ImmutableError{
|
||||
From: from,
|
||||
To: to,
|
||||
Key: key,
|
||||
error: errors.Errorf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", key, from, to),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ImmutableError) Error() string {
|
||||
return fmt.Sprintf("immutable configuration key \"%s\" was changed from \"%v\" to \"%v\"", e.Key, e.From, e.To)
|
||||
}
|
||||
51
oryx/configx/helpers.go
Normal file
51
oryx/configx/helpers.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// RegisterFlags registers the config file flag.
|
||||
func RegisterFlags(flags *pflag.FlagSet) {
|
||||
flags.StringSliceP("config", "c", []string{}, "Path to one or more .json, .yaml, .yml, .toml config files. Values are loaded in the order provided, meaning that the last config file overwrites values from the previous config file.")
|
||||
}
|
||||
|
||||
// host = unix:/path/to/socket => port is discarded, otherwise format as host:port
|
||||
func GetAddress(host string, port int) string {
|
||||
if strings.HasPrefix(host, "unix:") {
|
||||
return host
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", host, port)
|
||||
}
|
||||
|
||||
func (s *Serve) GetAddress() string {
|
||||
return GetAddress(s.Host, s.Port)
|
||||
}
|
||||
|
||||
// AddSchemaResources adds the config schema partials to the compiler.
|
||||
// The interface is specified instead of `jsonschema.Compiler` to allow the use of any jsonschema library fork or version.
|
||||
func AddSchemaResources(c interface {
|
||||
AddResource(url string, r io.Reader) error
|
||||
}) error {
|
||||
if err := c.AddResource(TLSConfigSchemaID, bytes.NewReader(TLSConfigSchema)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.AddResource(ServeConfigSchemaID, bytes.NewReader(ServeConfigSchema)); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.AddResource(CORSConfigSchemaID, bytes.NewReader(CORSConfigSchema))
|
||||
}
|
||||
|
||||
func cleanPrefix(prefix string) string {
|
||||
if len(prefix) > 0 {
|
||||
prefix = strings.TrimRight(prefix, ".") + "."
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
69
oryx/configx/koanf_confmap.go
Normal file
69
oryx/configx/koanf_confmap.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/knadh/koanf/maps"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// KoanfConfmap implements a raw map[string]interface{} provider.
|
||||
type KoanfConfmap struct {
|
||||
tuples []tuple
|
||||
}
|
||||
|
||||
// Provider returns a confmap Provider that takes a flat or nested
|
||||
// map[string]interface{}. If a delim is provided, it indicates that the
|
||||
// keys are flat and the map needs to be unflatted by delim.
|
||||
func NewKoanfConfmap(tuples []tuple) *KoanfConfmap {
|
||||
return &KoanfConfmap{tuples: jsonify(tuples)}
|
||||
}
|
||||
|
||||
func jsonify(tuples []tuple) []tuple {
|
||||
for k, t := range tuples {
|
||||
var parsed interface{}
|
||||
switch vt := t.Value.(type) {
|
||||
case string:
|
||||
if gjson.Valid(vt) && json.NewDecoder(bytes.NewBufferString(vt)).Decode(&parsed) == nil {
|
||||
tuples[k].Value = parsed
|
||||
}
|
||||
continue
|
||||
case []byte:
|
||||
if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil {
|
||||
tuples[k].Value = parsed
|
||||
}
|
||||
continue
|
||||
case json.RawMessage:
|
||||
if gjson.ValidBytes(vt) && json.NewDecoder(bytes.NewBuffer(vt)).Decode(&parsed) == nil {
|
||||
tuples[k].Value = parsed
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return tuples
|
||||
}
|
||||
|
||||
// ReadBytes is not supported by the env provider.
|
||||
func (e *KoanfConfmap) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("confmap provider does not support this method")
|
||||
}
|
||||
|
||||
// Read returns the loaded map[string]interface{}.
|
||||
func (e *KoanfConfmap) Read() (map[string]interface{}, error) {
|
||||
values := map[string]interface{}{}
|
||||
for _, t := range e.tuples {
|
||||
values[t.Key] = t.Value
|
||||
}
|
||||
|
||||
// Ensure any nested values are properly converted as well
|
||||
cp := maps.Copy(values)
|
||||
maps.IntfaceKeysToStrings(cp)
|
||||
cp = maps.Unflatten(cp, Delimiter)
|
||||
|
||||
return cp, nil
|
||||
}
|
||||
185
oryx/configx/koanf_env.go
Normal file
185
oryx/configx/koanf_env.go
Normal file
@@ -0,0 +1,185 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/x/castx"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
)
|
||||
|
||||
var isNumRegex = regexp.MustCompile("^[0-9]+$")
|
||||
|
||||
func NewKoanfEnv(prefix string, rawSchema []byte, schema *jsonschema.Schema) (*Env, error) {
|
||||
paths, err := getSchemaPaths(rawSchema, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Env{
|
||||
paths: paths,
|
||||
prefix: prefix,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Env implements an environment variables provider.
|
||||
type Env struct {
|
||||
prefix string
|
||||
paths []jsonschemax.Path
|
||||
}
|
||||
|
||||
// ReadBytes is not supported by the env provider.
|
||||
func (e *Env) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("env provider does not support this method")
|
||||
}
|
||||
|
||||
// Read reads all available environment variables into a key:value map
|
||||
// and returns it.
|
||||
func (e *Env) Read() (map[string]interface{}, error) {
|
||||
// Collect the environment variable keys.
|
||||
var keys []string
|
||||
for _, k := range os.Environ() {
|
||||
if e.prefix != "" {
|
||||
if strings.HasPrefix(k, e.prefix) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
} else {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
|
||||
raw := "{}"
|
||||
var err error
|
||||
for _, k := range keys {
|
||||
parts := strings.SplitN(k, "=", 2)
|
||||
|
||||
key, value := e.extract(parts[0], parts[1])
|
||||
// If the callback blanked the key, it should be omitted
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
raw, err = sjson.Set(raw, key, value)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(raw), &m); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Watch is not supported.
|
||||
func (e *Env) Watch(cb func(event interface{}, err error)) error {
|
||||
return errors.New("env provider does not support this method")
|
||||
}
|
||||
|
||||
func (e *Env) extract(key string, value string) (string, interface{}) {
|
||||
key = strings.Replace(strings.ToLower(strings.TrimPrefix(key, e.prefix)), "_", ".", -1)
|
||||
|
||||
for _, path := range e.paths {
|
||||
normalized := strings.Replace(path.Name, "_", ".", -1)
|
||||
name := path.Name
|
||||
|
||||
// Crazy hack to get arrays working.
|
||||
var indices []string
|
||||
searchParts := strings.Split(normalized, ".")
|
||||
keyParts := strings.Split(key, ".")
|
||||
if len(searchParts) == len(keyParts) {
|
||||
for k, search := range searchParts {
|
||||
if search != keyParts[k] {
|
||||
indices = nil
|
||||
}
|
||||
|
||||
if search != "#" {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isNumRegex.MatchString(keyParts[k]) {
|
||||
continue
|
||||
}
|
||||
|
||||
searchParts[k] = keyParts[k]
|
||||
indices = append(indices, keyParts[k])
|
||||
}
|
||||
}
|
||||
|
||||
if len(indices) > 0 {
|
||||
normalized = strings.Join(searchParts, ".")
|
||||
for _, index := range indices {
|
||||
name = strings.Replace(name, "#", index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if normalized == key {
|
||||
switch path.TypeHint {
|
||||
case jsonschemax.String:
|
||||
return name, cast.ToString(value)
|
||||
case jsonschemax.Float:
|
||||
return name, cast.ToFloat64(value)
|
||||
case jsonschemax.Int:
|
||||
return name, cast.ToInt64(value)
|
||||
case jsonschemax.Bool:
|
||||
return name, cast.ToBool(value)
|
||||
case jsonschemax.Nil:
|
||||
return name, nil
|
||||
case jsonschemax.BoolSlice:
|
||||
if !gjson.Valid(value) {
|
||||
return name, cast.ToBoolSlice(value)
|
||||
}
|
||||
fallthrough
|
||||
case jsonschemax.StringSlice:
|
||||
if !gjson.Valid(value) {
|
||||
return name, castx.ToStringSlice(value)
|
||||
}
|
||||
fallthrough
|
||||
case jsonschemax.IntSlice:
|
||||
if !gjson.Valid(value) {
|
||||
return name, cast.ToIntSlice(value)
|
||||
}
|
||||
fallthrough
|
||||
case jsonschemax.FloatSlice:
|
||||
if !gjson.Valid(value) {
|
||||
return name, castx.ToFloatSlice(value)
|
||||
}
|
||||
fallthrough
|
||||
case jsonschemax.JSON:
|
||||
return name, decode(value)
|
||||
default:
|
||||
return name, value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func decode(value string) (v interface{}) {
|
||||
b := []byte(value)
|
||||
var arr []interface{}
|
||||
if err := json.Unmarshal(b, &arr); err == nil {
|
||||
return &arr
|
||||
}
|
||||
h := map[string]interface{}{}
|
||||
if err := json.Unmarshal(b, &h); err == nil {
|
||||
return &h
|
||||
}
|
||||
return nil
|
||||
}
|
||||
90
oryx/configx/koanf_file.go
Normal file
90
oryx/configx/koanf_file.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/parsers/yaml"
|
||||
"github.com/knadh/koanf/v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/x/watcherx"
|
||||
)
|
||||
|
||||
// KoanfFile implements a KoanfFile provider.
|
||||
type KoanfFile struct {
|
||||
subKey string
|
||||
path string
|
||||
parser koanf.Parser
|
||||
}
|
||||
|
||||
// NewKoanfFile returns a file provider.
|
||||
func NewKoanfFile(path string) (*KoanfFile, error) {
|
||||
return NewKoanfFileSubKey(path, "")
|
||||
}
|
||||
|
||||
func NewKoanfFileSubKey(path, subKey string) (*KoanfFile, error) {
|
||||
kf := &KoanfFile{
|
||||
path: filepath.Clean(path),
|
||||
subKey: subKey,
|
||||
}
|
||||
|
||||
switch e := filepath.Ext(path); e {
|
||||
case ".toml":
|
||||
kf.parser = toml.Parser()
|
||||
case ".json":
|
||||
kf.parser = json.Parser()
|
||||
case ".yaml", ".yml":
|
||||
kf.parser = yaml.Parser()
|
||||
default:
|
||||
return nil, errors.Errorf("unknown config file extension: %s", e)
|
||||
}
|
||||
|
||||
return kf, nil
|
||||
}
|
||||
|
||||
// ReadBytes is not supported by KoanfFile.
|
||||
func (f *KoanfFile) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("file provider does not support this method")
|
||||
}
|
||||
|
||||
// Read reads the file and returns the parsed configuration.
|
||||
func (f *KoanfFile) Read() (map[string]interface{}, error) {
|
||||
//#nosec G304 -- false positive
|
||||
fc, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
v, err := f.parser.Unmarshal(fc)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
if f.subKey == "" {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
path := strings.Split(f.subKey, Delimiter)
|
||||
for i := range path {
|
||||
v = map[string]interface{}{
|
||||
path[len(path)-1-i]: v,
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// WatchChannel watches the file and triggers a callback when it changes. It is a
|
||||
// blocking function that internally spawns a goroutine to watch for changes.
|
||||
func (f *KoanfFile) WatchChannel(ctx context.Context, c watcherx.EventChannel) (watcherx.Watcher, error) {
|
||||
return watcherx.WatchFile(ctx, f.path, c)
|
||||
}
|
||||
35
oryx/configx/koanf_full_merge.go
Normal file
35
oryx/configx/koanf_full_merge.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"github.com/ory/x/jsonx"
|
||||
)
|
||||
|
||||
func MergeAllTypes(src, dst map[string]interface{}) error {
|
||||
rawSrc, err := json.Marshal(src)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
dstSrc, err := json.Marshal(dst)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
keys := jsonx.Flatten(rawSrc)
|
||||
for key, value := range keys {
|
||||
dstSrc, err = sjson.SetBytes(dstSrc, key, value)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return errors.WithStack(json.Unmarshal(dstSrc, &dst))
|
||||
}
|
||||
46
oryx/configx/koanf_memory.go
Normal file
46
oryx/configx/koanf_memory.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/v2"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
stdjson "encoding/json"
|
||||
)
|
||||
|
||||
// KoanfMemory implements a KoanfMemory provider.
|
||||
type KoanfMemory struct {
|
||||
doc stdjson.RawMessage
|
||||
parser koanf.Parser
|
||||
}
|
||||
|
||||
// NewKoanfMemory returns a file provider.
|
||||
func NewKoanfMemory(doc stdjson.RawMessage) *KoanfMemory {
|
||||
return &KoanfMemory{
|
||||
doc: doc,
|
||||
parser: json.Parser(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *KoanfMemory) SetDoc(doc stdjson.RawMessage) {
|
||||
f.doc = doc
|
||||
}
|
||||
|
||||
// ReadBytes reads the contents of a file on disk and returns the bytes.
|
||||
func (f *KoanfMemory) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("file provider does not support this method")
|
||||
}
|
||||
|
||||
// Read is not supported by the file provider.
|
||||
func (f *KoanfMemory) Read() (map[string]interface{}, error) {
|
||||
v, err := f.parser.Unmarshal(f.doc)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
53
oryx/configx/koanf_schema_defaults.go
Normal file
53
oryx/configx/koanf_schema_defaults.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/maps"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
)
|
||||
|
||||
type KoanfSchemaDefaults struct {
|
||||
keys []jsonschemax.Path
|
||||
}
|
||||
|
||||
func NewKoanfSchemaDefaults(rawSchema []byte, schema *jsonschema.Schema) (*KoanfSchemaDefaults, error) {
|
||||
keys, err := getSchemaPaths(rawSchema, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &KoanfSchemaDefaults{keys: keys}, nil
|
||||
}
|
||||
|
||||
func (k *KoanfSchemaDefaults) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("schema defaults provider does not support this method")
|
||||
}
|
||||
|
||||
func (k *KoanfSchemaDefaults) Read() (map[string]interface{}, error) {
|
||||
values := map[string]interface{}{}
|
||||
for _, key := range k.keys {
|
||||
// It's an array!
|
||||
if strings.Contains(key.Name, "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if key.Default != nil {
|
||||
values[key.Name] = key.Default
|
||||
}
|
||||
}
|
||||
|
||||
// Deep-copy: getSchemaPaths caches Path entries by schema hash, so
|
||||
// key.Default may be a map/slice shared across all callers. koanf.Merge
|
||||
// retains references when merging, and recursively writes into shared
|
||||
// sub-maps when a later provider overlaps the default's path —
|
||||
// without this copy, concurrent configx.New calls race on the shared
|
||||
// default value.
|
||||
return maps.Unflatten(maps.Copy(values), "."), nil
|
||||
}
|
||||
159
oryx/configx/options.go
Normal file
159
oryx/configx/options.go
Normal file
@@ -0,0 +1,159 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/x/logrusx"
|
||||
|
||||
"github.com/knadh/koanf/v2"
|
||||
|
||||
"github.com/ory/x/watcherx"
|
||||
)
|
||||
|
||||
type (
|
||||
OptionModifier func(p *Provider)
|
||||
)
|
||||
|
||||
func WithContext(ctx context.Context) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
for _, o := range ConfigOptionsFromContext(ctx) {
|
||||
o(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithConfigFiles(files ...string) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.files = append(p.files, files...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithImmutables(immutables ...string) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.immutables = append(p.immutables, immutables...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithExceptImmutables(exceptImmutables ...string) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.exceptImmutables = append(p.exceptImmutables, exceptImmutables...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithFlags(flags *pflag.FlagSet) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.flags = flags
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogger(l *logrusx.Logger) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.logger = l
|
||||
}
|
||||
}
|
||||
|
||||
func SkipValidation() OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.skipValidation = true
|
||||
}
|
||||
}
|
||||
|
||||
func DisableEnvLoading() OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.disableEnvLoading = true
|
||||
}
|
||||
}
|
||||
|
||||
func WithValue(key string, value interface{}) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value})
|
||||
}
|
||||
}
|
||||
|
||||
func WithValues(values map[string]interface{}) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
for key, value := range values {
|
||||
p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithBaseValues(values map[string]interface{}) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
for key, value := range values {
|
||||
p.baseValues = append(p.baseValues, tuple{Key: key, Value: value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithUserProviders(providers ...koanf.Provider) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.userProviders = providers
|
||||
}
|
||||
}
|
||||
|
||||
// DEPRECATED without replacement. This option is a no-op.
|
||||
func OmitKeysFromTracing(keys ...string) OptionModifier {
|
||||
return func(*Provider) {}
|
||||
}
|
||||
|
||||
func AttachWatcher(watcher func(event watcherx.Event, err error)) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.onChanges = append(p.onChanges, watcher)
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogrusWatcher(l *logrusx.Logger) OptionModifier {
|
||||
return AttachWatcher(LogrusWatcher(l))
|
||||
}
|
||||
|
||||
func LogrusWatcher(l *logrusx.Logger) func(e watcherx.Event, err error) {
|
||||
return func(e watcherx.Event, err error) {
|
||||
l.WithField("file", e.Source()).
|
||||
WithField("event_type", fmt.Sprintf("%T", e)).
|
||||
Info("A change to a configuration file was detected.")
|
||||
|
||||
if et := new(jsonschema.ValidationError); errors.As(err, &et) {
|
||||
l.WithField("event", fmt.Sprintf("%#v", et)).
|
||||
Errorf("The changed configuration is invalid and could not be loaded. Rolling back to the last working configuration revision. Please address the validation errors before restarting the process.")
|
||||
} else if et := new(ImmutableError); errors.As(err, &et) {
|
||||
l.WithError(err).
|
||||
WithField("key", et.Key).
|
||||
WithField("old_value", fmt.Sprintf("%v", et.From)).
|
||||
WithField("new_value", fmt.Sprintf("%v", et.To)).
|
||||
Errorf("A configuration value marked as immutable has changed. Rolling back to the last working configuration revision. To reload the values please restart the process.")
|
||||
} else if err != nil {
|
||||
l.WithError(err).Errorf("An error occurred while watching config file %s", e.Source())
|
||||
} else {
|
||||
l.WithField("file", e.Source()).
|
||||
WithField("event_type", fmt.Sprintf("%T", e)).
|
||||
Info("Configuration change processed successfully.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithStderrValidationReporter() OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.onValidationError = func(k *koanf.Koanf, err error) {
|
||||
p.printHumanReadableValidationErrors(k, os.Stderr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WithStandardValidationReporter(w io.Writer) OptionModifier {
|
||||
return func(p *Provider) {
|
||||
p.onValidationError = func(k *koanf.Koanf, err error) {
|
||||
p.printHumanReadableValidationErrors(k, w, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
oryx/configx/permission.go
Normal file
56
oryx/configx/permission.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type UnixPermission struct {
|
||||
Owner string
|
||||
Group string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
func (p *UnixPermission) SetPermission(file string) error {
|
||||
var e error
|
||||
e = os.Chmod(file, p.Mode)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
gid := -1
|
||||
uid := -1
|
||||
|
||||
if p.Owner != "" {
|
||||
var userObj *user.User
|
||||
userObj, e = user.Lookup(p.Owner)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
uid, e = strconv.Atoi(userObj.Uid)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
if p.Group != "" {
|
||||
var group *user.Group
|
||||
group, e := user.LookupGroup(p.Group)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
gid, e = strconv.Atoi(group.Gid)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
}
|
||||
|
||||
e = os.Chown(file, uid, gid)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
oryx/configx/pflag.go
Normal file
57
oryx/configx/pflag.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
)
|
||||
|
||||
type PFlagProvider struct {
|
||||
p *posflag.Posflag
|
||||
paths []jsonschemax.Path
|
||||
}
|
||||
|
||||
func NewPFlagProvider(rawSchema []byte, schema *jsonschema.Schema, f *pflag.FlagSet, k *koanf.Koanf) (*PFlagProvider, error) {
|
||||
paths, err := getSchemaPaths(rawSchema, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PFlagProvider{
|
||||
p: posflag.Provider(f, ".", k),
|
||||
paths: paths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *PFlagProvider) ReadBytes() ([]byte, error) {
|
||||
return nil, errors.New("pflag provider does not support this method")
|
||||
}
|
||||
|
||||
func (p *PFlagProvider) Read() (map[string]interface{}, error) {
|
||||
all, err := p.p.Read()
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
knownFlags := make(map[string]interface{}, len(all))
|
||||
for k, v := range all {
|
||||
k = strings.ReplaceAll(k, ".", "-")
|
||||
for _, path := range p.paths {
|
||||
normalized := strings.ReplaceAll(path.Name, ".", "-")
|
||||
if k == normalized {
|
||||
knownFlags[k] = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return knownFlags, nil
|
||||
}
|
||||
|
||||
var _ koanf.Provider = (*PFlagProvider)(nil)
|
||||
561
oryx/configx/provider.go
Normal file
561
oryx/configx/provider.go
Normal file
@@ -0,0 +1,561 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/inhies/go-bytesize"
|
||||
"github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/posflag"
|
||||
"github.com/knadh/koanf/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
"github.com/ory/x/jsonschemax"
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/watcherx"
|
||||
)
|
||||
|
||||
type tuple struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
l sync.RWMutex
|
||||
*koanf.Koanf
|
||||
immutables, exceptImmutables []string
|
||||
|
||||
schema []byte
|
||||
flags *pflag.FlagSet
|
||||
validator *jsonschema.Schema
|
||||
onChanges []func(watcherx.Event, error)
|
||||
onValidationError func(k *koanf.Koanf, err error)
|
||||
|
||||
forcedValues []tuple
|
||||
baseValues []tuple
|
||||
files []string
|
||||
|
||||
skipValidation bool
|
||||
disableEnvLoading bool
|
||||
|
||||
logger *logrusx.Logger
|
||||
|
||||
providers []koanf.Provider
|
||||
userProviders []koanf.Provider
|
||||
}
|
||||
|
||||
const (
|
||||
FlagConfig = "config"
|
||||
Delimiter = "."
|
||||
)
|
||||
|
||||
// RegisterConfigFlag registers the "--config" flag on pflag.FlagSet.
|
||||
func RegisterConfigFlag(flags *pflag.FlagSet, fallback []string) {
|
||||
flags.StringSliceP(FlagConfig, "c", fallback, "Config files to load, overwriting in the order specified.")
|
||||
}
|
||||
|
||||
// New creates a new provider instance or errors.
|
||||
// Configuration values are loaded in the following order:
|
||||
//
|
||||
// 1. Defaults from the JSON Schema
|
||||
// 2. Config files (yaml, yml, toml, json)
|
||||
// 3. Command line flags
|
||||
// 4. Environment variables
|
||||
//
|
||||
// There will also be file-watchers started for all config files. To cancel the
|
||||
// watchers, cancel the context.
|
||||
func New(ctx context.Context, schema []byte, modifiers ...OptionModifier) (*Provider, error) {
|
||||
validator, err := getSchema(ctx, schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := logrus.New()
|
||||
l.Out = io.Discard
|
||||
|
||||
p := &Provider{
|
||||
schema: schema,
|
||||
validator: validator,
|
||||
onValidationError: func(k *koanf.Koanf, err error) {},
|
||||
logger: logrusx.New("discarding config logger", "", logrusx.UseLogger(l)),
|
||||
Koanf: koanf.NewWithConf(koanf.Conf{Delim: Delimiter, StrictMerge: true}),
|
||||
}
|
||||
|
||||
for _, m := range modifiers {
|
||||
m(p)
|
||||
}
|
||||
|
||||
providers, err := p.createProviders(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.providers = providers
|
||||
|
||||
k, err := p.newKoanf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.replaceKoanf(k)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *Provider) SkipValidation() bool {
|
||||
return p.skipValidation
|
||||
}
|
||||
|
||||
func (p *Provider) createProviders(ctx context.Context) (providers []koanf.Provider, err error) {
|
||||
defaultsProvider, err := NewKoanfSchemaDefaults(p.schema, p.validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, defaultsProvider)
|
||||
|
||||
// Workaround for https://github.com/knadh/koanf/pull/47
|
||||
for _, t := range p.baseValues {
|
||||
providers = append(providers, NewKoanfConfmap([]tuple{t}))
|
||||
}
|
||||
|
||||
paths := p.files
|
||||
if p.flags != nil {
|
||||
p, _ := p.flags.GetStringSlice(FlagConfig)
|
||||
paths = append(paths, p...)
|
||||
}
|
||||
|
||||
p.logger.WithField("files", paths).Debug("Adding config files.")
|
||||
|
||||
c := make(watcherx.EventChannel)
|
||||
|
||||
defer func() {
|
||||
if err == nil && len(paths) > 0 {
|
||||
go p.watchForFileChanges(ctx, c)
|
||||
}
|
||||
}()
|
||||
for _, path := range paths {
|
||||
fp, err := NewKoanfFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := fp.WatchChannel(ctx, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers = append(providers, fp)
|
||||
}
|
||||
|
||||
providers = append(providers, p.userProviders...)
|
||||
|
||||
if p.flags != nil {
|
||||
pp, err := NewPFlagProvider(p.schema, p.validator, p.flags, p.Koanf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, pp)
|
||||
}
|
||||
|
||||
if !p.disableEnvLoading {
|
||||
envProvider, err := NewKoanfEnv("", p.schema, p.validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, envProvider)
|
||||
}
|
||||
|
||||
// Workaround for https://github.com/knadh/koanf/pull/47
|
||||
for _, t := range p.forcedValues {
|
||||
providers = append(providers, NewKoanfConfmap([]tuple{t}))
|
||||
}
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (p *Provider) replaceKoanf(k *koanf.Koanf) {
|
||||
p.Koanf = k
|
||||
}
|
||||
|
||||
func (p *Provider) validate(k *koanf.Koanf) error {
|
||||
if p.skipValidation {
|
||||
return nil
|
||||
}
|
||||
|
||||
out, err := k.Marshal(json.Parser())
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err := p.validator.Validate(bytes.NewReader(out)); err != nil {
|
||||
p.onValidationError(k, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// newKoanf creates a new koanf instance with all the updated config
|
||||
//
|
||||
// This is unfortunately required due to several limitations / bugs in koanf:
|
||||
//
|
||||
// - https://github.com/knadh/koanf/issues/77
|
||||
// - https://github.com/knadh/koanf/pull/47
|
||||
func (p *Provider) newKoanf() (_ *koanf.Koanf, err error) {
|
||||
k := koanf.New(Delimiter)
|
||||
|
||||
for _, provider := range p.providers {
|
||||
// posflag.Posflag requires access to Koanf instance so we recreate the provider here which is a workaround
|
||||
// for posflag.Provider's API.
|
||||
if _, ok := provider.(*posflag.Posflag); ok {
|
||||
provider = posflag.Provider(p.flags, ".", k)
|
||||
}
|
||||
|
||||
var opts []koanf.Option
|
||||
if _, ok := provider.(*Env); ok {
|
||||
opts = append(opts, koanf.WithMergeFunc(MergeAllTypes))
|
||||
}
|
||||
|
||||
if err := k.Load(provider, nil, opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.validate(k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// SetTracer does nothing. DEPRECATED without replacement.
|
||||
func (p *Provider) SetTracer(_ context.Context, _ *otelx.Tracer) {
|
||||
}
|
||||
|
||||
func (p *Provider) runOnChanges(e watcherx.Event, err error) {
|
||||
for k := range p.onChanges {
|
||||
p.onChanges[k](e, err)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteOtherKeys(k *koanf.Koanf, keys []string) {
|
||||
outer:
|
||||
for _, key := range k.Keys() {
|
||||
for _, ik := range keys {
|
||||
if key == ik {
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
k.Delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) reload(e watcherx.Event) {
|
||||
p.l.Lock()
|
||||
|
||||
var err error
|
||||
defer func() {
|
||||
// we first want to unlock and then runOnChanges, so that the callbacks can actually use the Provider
|
||||
p.l.Unlock()
|
||||
p.runOnChanges(e, err)
|
||||
}()
|
||||
|
||||
nk, err := p.newKoanf()
|
||||
if err != nil {
|
||||
return // unlocks & runs changes in defer
|
||||
}
|
||||
|
||||
oldImmutables, newImmutables := p.Koanf.Copy(), nk.Copy()
|
||||
deleteOtherKeys(oldImmutables, p.immutables)
|
||||
deleteOtherKeys(newImmutables, p.immutables)
|
||||
|
||||
for _, key := range p.exceptImmutables {
|
||||
oldImmutables.Delete(key)
|
||||
newImmutables.Delete(key)
|
||||
}
|
||||
if !reflect.DeepEqual(oldImmutables.Raw(), newImmutables.Raw()) {
|
||||
for _, key := range p.immutables {
|
||||
if !reflect.DeepEqual(oldImmutables.Get(key), newImmutables.Get(key)) {
|
||||
err = NewImmutableError(key, fmt.Sprintf("%v", p.Koanf.Get(key)), fmt.Sprintf("%v", nk.Get(key)))
|
||||
return // unlocks & runs changes in defer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.replaceKoanf(nk)
|
||||
|
||||
// unlocks & runs changes in defer
|
||||
}
|
||||
|
||||
func (p *Provider) watchForFileChanges(ctx context.Context, c watcherx.EventChannel) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case e := <-c:
|
||||
switch et := e.(type) {
|
||||
case *watcherx.ErrorEvent:
|
||||
p.runOnChanges(e, et)
|
||||
default:
|
||||
p.reload(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DirtyPatch patches individual config keys without reloading the full config
|
||||
//
|
||||
// WARNING! This method is only useful to override existing keys in string or number
|
||||
// format. DO NOT use this method to override arrays, maps, or other complex types.
|
||||
//
|
||||
// This method DOES NOT validate the config against the config JSON schema. If you
|
||||
// need to validate the config, use the Set method instead.
|
||||
//
|
||||
// This method can not be used to remove keys from the config as that is not
|
||||
// possible without reloading the full config.
|
||||
func (p *Provider) DirtyPatch(key string, value any) error {
|
||||
p.l.Lock()
|
||||
defer p.l.Unlock()
|
||||
|
||||
t := tuple{Key: key, Value: value}
|
||||
kc := NewKoanfConfmap([]tuple{t})
|
||||
|
||||
p.forcedValues = append(p.forcedValues, t)
|
||||
p.providers = append(p.providers, kc)
|
||||
|
||||
if err := p.Koanf.Load(kc, nil, []koanf.Option{}...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) Set(key string, value interface{}) error {
|
||||
p.l.Lock()
|
||||
defer p.l.Unlock()
|
||||
|
||||
p.forcedValues = append(p.forcedValues, tuple{Key: key, Value: value})
|
||||
p.providers = append(p.providers, NewKoanfConfmap([]tuple{{Key: key, Value: value}}))
|
||||
|
||||
k, err := p.newKoanf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.replaceKoanf(k)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigFiles returns all config file paths configured on this provider,
|
||||
// including paths set via WithConfigFiles and via the --config flag.
|
||||
func (p *Provider) ConfigFiles() []string {
|
||||
paths := make([]string, len(p.files))
|
||||
copy(paths, p.files)
|
||||
if p.flags != nil {
|
||||
flagPaths, _ := p.flags.GetStringSlice(FlagConfig)
|
||||
paths = append(paths, flagPaths...)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
func (p *Provider) BoolF(key string, fallback bool) bool {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Bool(key)
|
||||
}
|
||||
|
||||
func (p *Provider) StringF(key string, fallback string) string {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.String(key)
|
||||
}
|
||||
|
||||
func (p *Provider) StringsF(key string, fallback []string) (val []string) {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Strings(key)
|
||||
}
|
||||
|
||||
func (p *Provider) IntF(key string, fallback int) (val int) {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Int(key)
|
||||
}
|
||||
|
||||
func (p *Provider) Float64F(key string, fallback float64) (val float64) {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Float64(key)
|
||||
}
|
||||
|
||||
func (p *Provider) DurationF(key string, fallback time.Duration) (val time.Duration) {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Duration(key)
|
||||
}
|
||||
|
||||
func (p *Provider) ByteSizeF(key string, fallback bytesize.ByteSize) bytesize.ByteSize {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Koanf.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
switch v := p.Koanf.Get(key).(type) {
|
||||
case string:
|
||||
// this type usually comes from user input
|
||||
dec, err := bytesize.Parse(v)
|
||||
if err != nil {
|
||||
p.logger.WithField("key", key).WithField("raw_value", v).WithError(err).Warnf("error parsing byte size value, using fallback of %s", fallback)
|
||||
return fallback
|
||||
}
|
||||
return dec
|
||||
case float64:
|
||||
// this type comes from json.Unmarshal
|
||||
return bytesize.ByteSize(v)
|
||||
case bytesize.ByteSize:
|
||||
return v
|
||||
default:
|
||||
p.logger.WithField("key", key).WithField("raw_type", fmt.Sprintf("%T", v)).WithField("raw_value", fmt.Sprintf("%+v", v)).Errorf("error converting byte size value because of unknown type, using fallback of %s", fallback)
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) GetF(key string, fallback interface{}) (val interface{}) {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
if !p.Exists(key) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return p.Get(key)
|
||||
}
|
||||
|
||||
func (p *Provider) TracingConfig(serviceName string) *otelx.Config {
|
||||
return &otelx.Config{
|
||||
ServiceName: p.StringF("tracing.service_name", serviceName),
|
||||
DeploymentEnvironment: p.StringF("tracing.deployment_environment", ""),
|
||||
Provider: p.String("tracing.provider"),
|
||||
Providers: otelx.ProvidersConfig{
|
||||
Jaeger: otelx.JaegerConfig{
|
||||
Sampling: otelx.JaegerSampling{
|
||||
ServerURL: p.String("tracing.providers.jaeger.sampling.server_url"),
|
||||
TraceIDRatio: p.Float64F("tracing.providers.jaeger.sampling.trace_id_ratio", 1),
|
||||
},
|
||||
LocalAgentAddress: p.String("tracing.providers.jaeger.local_agent_address"),
|
||||
},
|
||||
Zipkin: otelx.ZipkinConfig{
|
||||
ServerURL: p.String("tracing.providers.zipkin.server_url"),
|
||||
Sampling: otelx.ZipkinSampling{
|
||||
SamplingRatio: p.Float64("tracing.providers.zipkin.sampling.sampling_ratio"),
|
||||
},
|
||||
},
|
||||
OTLP: otelx.OTLPConfig{
|
||||
ServerURL: p.String("tracing.providers.otlp.server_url"),
|
||||
Insecure: p.Bool("tracing.providers.otlp.insecure"),
|
||||
Sampling: otelx.OTLPSampling{
|
||||
SamplingRatio: p.Float64F("tracing.providers.otlp.sampling.sampling_ratio", 1),
|
||||
},
|
||||
AuthorizationHeader: p.String("tracing.providers.otlp.authorization_header"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) RequestURIF(path string, fallback *url.URL) *url.URL {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
switch t := p.Get(path).(type) {
|
||||
case *url.URL:
|
||||
return t
|
||||
case url.URL:
|
||||
return &t
|
||||
case string:
|
||||
if parsed, err := url.ParseRequestURI(t); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (p *Provider) URIF(path string, fallback *url.URL) *url.URL {
|
||||
p.l.RLock()
|
||||
defer p.l.RUnlock()
|
||||
|
||||
switch t := p.Get(path).(type) {
|
||||
case *url.URL:
|
||||
return t
|
||||
case url.URL:
|
||||
return &t
|
||||
case string:
|
||||
if parsed, err := url.Parse(t); err == nil {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// PrintHumanReadableValidationErrors prints human readable validation errors. Duh.
|
||||
func (p *Provider) PrintHumanReadableValidationErrors(w io.Writer, err error) {
|
||||
p.printHumanReadableValidationErrors(p.Koanf, w, err)
|
||||
}
|
||||
|
||||
func (p *Provider) printHumanReadableValidationErrors(k *koanf.Koanf, w io.Writer, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, "")
|
||||
conf, innerErr := k.Marshal(json.Parser())
|
||||
if innerErr != nil {
|
||||
_, _ = fmt.Fprintf(w, "Unable to unmarshal configuration: %+v", innerErr)
|
||||
}
|
||||
|
||||
jsonschemax.FormatValidationErrorForCLI(w, conf, err)
|
||||
}
|
||||
45
oryx/configx/schema.go
Normal file
45
oryx/configx/schema.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/otelx"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
)
|
||||
|
||||
func newCompiler(schema []byte) (string, *jsonschema.Compiler, error) {
|
||||
id := gjson.GetBytes(schema, "$id").String()
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("%s.json", uuid.Must(uuid.NewV4()).String())
|
||||
}
|
||||
|
||||
compiler := jsonschema.NewCompiler()
|
||||
if err := compiler.AddResource(id, bytes.NewReader(schema)); err != nil {
|
||||
return "", nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
// DO NOT REMOVE THIS
|
||||
compiler.ExtractAnnotations = true
|
||||
|
||||
if err := otelx.AddConfigSchema(compiler); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if err := logrusx.AddConfigSchema(compiler); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if err := AddSchemaResources(compiler); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return id, compiler, nil
|
||||
}
|
||||
48
oryx/configx/schema_cache.go
Normal file
48
oryx/configx/schema_cache.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
)
|
||||
|
||||
var schemaCacheConfig = &ristretto.Config[[]byte, *jsonschema.Schema]{
|
||||
// Hold up to 25 schemas in cache. Usually we only need one.
|
||||
MaxCost: 25,
|
||||
NumCounters: 250,
|
||||
BufferItems: 64,
|
||||
Metrics: false,
|
||||
IgnoreInternalCost: true,
|
||||
Cost: func(*jsonschema.Schema) int64 {
|
||||
return 1
|
||||
},
|
||||
}
|
||||
|
||||
var schemaCache, _ = ristretto.NewCache(schemaCacheConfig)
|
||||
|
||||
func getSchema(ctx context.Context, schema []byte) (*jsonschema.Schema, error) {
|
||||
key := sha256.Sum256(schema)
|
||||
if val, found := schemaCache.Get(key[:]); found {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
schemaID, comp, err := newCompiler(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
validator, err := comp.Compile(ctx, schemaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schemaCache.Set(key[:], validator, 1)
|
||||
schemaCache.Wait()
|
||||
return validator, nil
|
||||
}
|
||||
41
oryx/configx/schema_path_cache.go
Normal file
41
oryx/configx/schema_path_cache.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/ory/x/jsonschemax"
|
||||
|
||||
"github.com/dgraph-io/ristretto/v2"
|
||||
|
||||
"github.com/ory/jsonschema/v3"
|
||||
)
|
||||
|
||||
var schemaPathCacheConfig = &ristretto.Config[[]byte, []jsonschemax.Path]{
|
||||
// Hold up to 25 schemas in cache. Usually we only need one.
|
||||
MaxCost: 250,
|
||||
NumCounters: 2500,
|
||||
BufferItems: 64,
|
||||
Metrics: false,
|
||||
IgnoreInternalCost: true,
|
||||
}
|
||||
|
||||
var schemaPathCache, _ = ristretto.NewCache(schemaPathCacheConfig)
|
||||
|
||||
func getSchemaPaths(rawSchema []byte, schema *jsonschema.Schema) ([]jsonschemax.Path, error) {
|
||||
key := sha256.Sum256(rawSchema)
|
||||
if val, found := schemaPathCache.Get(key[:]); found {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
keys, err := jsonschemax.ListPathsWithInitializedSchemaAndArraysIncluded(schema)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
schemaPathCache.Set(key[:], keys, 1)
|
||||
schemaPathCache.Wait()
|
||||
return keys, nil
|
||||
}
|
||||
137
oryx/configx/serve.go
Normal file
137
oryx/configx/serve.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/ory/x/logrusx"
|
||||
"github.com/ory/x/tlsx"
|
||||
)
|
||||
|
||||
const (
|
||||
ServeConfigSchemaID = "ory://serve-config"
|
||||
TLSConfigSchemaID = "ory://tls-config"
|
||||
)
|
||||
|
||||
//go:embed serve.schema.json
|
||||
var ServeConfigSchema []byte
|
||||
|
||||
//go:embed tls.schema.json
|
||||
var TLSConfigSchema []byte
|
||||
|
||||
type (
|
||||
Serve struct {
|
||||
Host, WriteListenFile string
|
||||
Port int
|
||||
BaseURL *url.URL
|
||||
Socket UnixPermission
|
||||
TLS TLS
|
||||
RequestLog ServeRequestLog
|
||||
}
|
||||
TLS struct {
|
||||
Enabled bool
|
||||
AllowTerminationFrom []string
|
||||
CertBase64, KeyBase64, CertPath, KeyPath string
|
||||
}
|
||||
ServeRequestLog struct {
|
||||
DisableHealth bool
|
||||
}
|
||||
)
|
||||
|
||||
func (p *Provider) Serve(prefix string, isDev bool, defaults Serve) *Serve {
|
||||
prefix = cleanPrefix(prefix)
|
||||
|
||||
defaults.Socket.Mode = cmp.Or(defaults.Socket.Mode, 0o755)
|
||||
|
||||
serve := Serve{
|
||||
Host: p.StringF(prefix+"host", defaults.Host),
|
||||
Port: p.IntF(prefix+"port", defaults.Port),
|
||||
WriteListenFile: p.StringF(prefix+"write_listen_file", defaults.WriteListenFile),
|
||||
BaseURL: p.URIF(prefix+"base_url", defaults.BaseURL),
|
||||
Socket: UnixPermission{
|
||||
Owner: p.StringF(prefix+"socket.owner", defaults.Socket.Owner),
|
||||
Group: p.StringF(prefix+"socket.group", defaults.Socket.Group),
|
||||
Mode: os.FileMode(p.IntF(prefix+"socket.mode", int(defaults.Socket.Mode))),
|
||||
},
|
||||
TLS: p.TLS(prefix+"tls", defaults.TLS),
|
||||
RequestLog: ServeRequestLog{
|
||||
DisableHealth: p.BoolF(prefix+"request_log.disable_for_health", defaults.RequestLog.DisableHealth),
|
||||
},
|
||||
}
|
||||
|
||||
if serve.BaseURL == nil {
|
||||
serve.BaseURL = &url.URL{
|
||||
Scheme: "http",
|
||||
Path: "/",
|
||||
}
|
||||
if !isDev || serve.TLS.Enabled {
|
||||
serve.BaseURL.Scheme = "https"
|
||||
}
|
||||
host := serve.Host
|
||||
if host == "0.0.0.0" || host == "" {
|
||||
var err error
|
||||
host, err = os.Hostname()
|
||||
if err != nil {
|
||||
p.logger.WithError(err).Warn("Unable to get hostname from system, falling back to 127.0.0.1.")
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
}
|
||||
serve.BaseURL.Host = fmt.Sprintf("%s:%d", host, serve.Port)
|
||||
}
|
||||
|
||||
return &serve
|
||||
}
|
||||
|
||||
func (p *Provider) TLS(prefix string, defaults TLS) TLS {
|
||||
prefix = cleanPrefix(prefix)
|
||||
|
||||
return TLS{
|
||||
Enabled: p.BoolF(prefix+"enabled", defaults.Enabled),
|
||||
AllowTerminationFrom: p.StringsF(prefix+"allow_termination_from", defaults.AllowTerminationFrom),
|
||||
CertBase64: p.StringF(prefix+"cert.base64", defaults.CertBase64),
|
||||
KeyBase64: p.StringF(prefix+"key.base64", defaults.KeyBase64),
|
||||
CertPath: p.StringF(prefix+"cert.path", defaults.CertPath),
|
||||
KeyPath: p.StringF(prefix+"key.path", defaults.KeyPath),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TLS) GetCertFunc(ctx context.Context, l *logrusx.Logger, ifaceName string) (tlsx.CertFunc, error) {
|
||||
switch {
|
||||
case t.CertBase64 != "" && t.KeyBase64 != "":
|
||||
cert, err := tlsx.CertificateFromBase64(t.CertBase64, t.KeyBase64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err)
|
||||
}
|
||||
l.Infof("Setting up HTTPS for %s", ifaceName)
|
||||
return func(*tls.ClientHelloInfo) (*tls.Certificate, error) { return &cert, nil }, nil
|
||||
case t.CertPath != "" && t.KeyPath != "":
|
||||
errs := make(chan error, 1)
|
||||
getCert, err := tlsx.GetCertificate(ctx, t.CertPath, t.KeyPath, errs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load TLS certificate for interface %s: %w", ifaceName, err)
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case err := <-errs:
|
||||
l.WithError(err).Error("Failed to reload TLS certificates, using previous certificates")
|
||||
}
|
||||
}
|
||||
}()
|
||||
l.Infof("Setting up HTTPS for %s (automatic certificate reloading active)", ifaceName)
|
||||
return getCert, nil
|
||||
default:
|
||||
l.Infof("TLS has not been configured for %s, skipping", ifaceName)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
70
oryx/configx/serve.schema.json
Normal file
70
oryx/configx/serve.schema.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"$id": "https://github.com/ory/x/configx/serve.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"request_log": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"disable_for_health": {
|
||||
"title": "Disable health endpoints request logging",
|
||||
"description": "Disable request logging for /health/alive and /health/ready endpoints",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"base_url": {
|
||||
"title": "Base URL",
|
||||
"description": "The URL where the endpoint is exposed at. This domain is used to generate redirects, form URLs, and more.",
|
||||
"type": "string",
|
||||
"format": "uri-reference",
|
||||
"examples": [
|
||||
"https://my-app.com/",
|
||||
"https://my-app.com/.ory/kratos/public",
|
||||
"https://auth.my-app.com/hydra"
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"title": "Host",
|
||||
"description": "The host (interface) that the endpoint listens on.",
|
||||
"type": "string",
|
||||
"default": "0.0.0.0"
|
||||
},
|
||||
"port": {
|
||||
"title": "Port",
|
||||
"description": "The port that the endpoint listens on.",
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"socket": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Sets the permissions of the unix socket",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Owner of unix socket. If empty, the owner will be the user running the service.",
|
||||
"default": ""
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"description": "Group of unix socket. If empty, the group will be the primary group of the user running the service.",
|
||||
"default": ""
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer",
|
||||
"description": "Mode of unix socket in numeric form",
|
||||
"default": 493,
|
||||
"minimum": 0,
|
||||
"maximum": 511
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"$ref": "ory://tls-config"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
oryx/configx/span.go
Normal file
10
oryx/configx/span.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package configx
|
||||
|
||||
const (
|
||||
LoadSpanOpName = "config-load"
|
||||
UpdatedSpanOpName = "config-update"
|
||||
SnapshotSpanOpName = "config-snapshot"
|
||||
)
|
||||
312
oryx/configx/stub/benchmark/benchmark.yaml
Normal file
312
oryx/configx/stub/benchmark/benchmark.yaml
Normal file
@@ -0,0 +1,312 @@
|
||||
# Please find the documentation for this file at
|
||||
# https://www.ory.com/oathkeeper/docs/configuration
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: json
|
||||
|
||||
profiling: cpu
|
||||
|
||||
serve:
|
||||
proxy:
|
||||
port: 1234
|
||||
host: 127.0.0.1
|
||||
|
||||
timeout:
|
||||
read: 1s
|
||||
write: 2s
|
||||
idle: 3s
|
||||
|
||||
cors:
|
||||
enabled: true
|
||||
allowed_origins:
|
||||
- https://example.com
|
||||
- https://*.example.com
|
||||
allowed_methods:
|
||||
- POST
|
||||
- GET
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
allowed_headers:
|
||||
- Authorization
|
||||
- Content-Type
|
||||
exposed_headers:
|
||||
- Content-Type
|
||||
allow_credentials: true
|
||||
max_age: 10
|
||||
debug: true
|
||||
tls:
|
||||
key:
|
||||
path: /path/to/key.pem
|
||||
base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3...
|
||||
cert:
|
||||
path: /path/to/cert.pem
|
||||
base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr...
|
||||
|
||||
api:
|
||||
port: 1235
|
||||
host: 127.0.0.2
|
||||
|
||||
timeout:
|
||||
read: 1s
|
||||
write: 2s
|
||||
idle: 3s
|
||||
|
||||
cors:
|
||||
enabled: true
|
||||
allowed_origins:
|
||||
- https://example.org
|
||||
- https://*.example.org
|
||||
allowed_methods:
|
||||
- GET
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
allowed_headers:
|
||||
- Authorization
|
||||
- Content-Type
|
||||
exposed_headers:
|
||||
- Content-Type
|
||||
allow_credentials: true
|
||||
max_age: 10
|
||||
debug: true
|
||||
tls:
|
||||
key:
|
||||
path: /path/to/key.pem
|
||||
base64: LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3...
|
||||
cert:
|
||||
path: /path/to/cert.pem
|
||||
base64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr...
|
||||
|
||||
prometheus:
|
||||
port: 9000
|
||||
host: localhost
|
||||
metrics_path: /metrics
|
||||
collapse_request_paths: true
|
||||
|
||||
# Configures Access Rules
|
||||
access_rules:
|
||||
# Locations (list of URLs) where access rules should be fetched from on boot.
|
||||
# It is expected that the documents at those locations return a JSON or YAML Array containing ORY Oathkeeper Access Rules.
|
||||
repositories:
|
||||
# If the URL Scheme is `file://`, the access rules (an array of access rules is expected) will be
|
||||
# fetched from the local file system.
|
||||
- file://path/to/rules.json
|
||||
# If the URL Scheme is `inline://`, the access rules (an array of access rules is expected)
|
||||
# are expected to be a base64 encoded (with padding!) JSON/YAML string (base64_encode(`[{"id":"foo-rule","authenticators":[....]}]`)):
|
||||
- inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d
|
||||
# If the URL Scheme is `http://` or `https://`, the access rules (an array of access rules is expected) will be
|
||||
# fetched from the provided HTTP(s) location.
|
||||
- https://path-to-my-rules/rules.json
|
||||
# Optional fields describing matching strategy, defaults to "regexp".
|
||||
matching_strategy: glob
|
||||
|
||||
errors:
|
||||
fallback:
|
||||
- json
|
||||
handlers:
|
||||
redirect:
|
||||
enabled: true
|
||||
config:
|
||||
to: http://path-to/redirect
|
||||
json:
|
||||
enabled: true
|
||||
config:
|
||||
verbose: true
|
||||
when:
|
||||
- error:
|
||||
- unauthorized
|
||||
- forbidden
|
||||
- internal_server_error
|
||||
request:
|
||||
header:
|
||||
content_type:
|
||||
- application/json
|
||||
accept:
|
||||
- application/json
|
||||
cidr:
|
||||
- 127.0.0.0/24
|
||||
|
||||
# All authenticators can be configured under this configuration key
|
||||
authenticators:
|
||||
# Configures the anonymous authenticator
|
||||
anonymous:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# Sets the anonymous username. Defaults to "anonymous". Common names include "guest", "anon", "anonymous", "unknown".
|
||||
subject: guest
|
||||
|
||||
# Configures the cookie session authenticator
|
||||
cookie_session:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# Sets the origin to proxy requests to. If the response is a 200 with body `{ "subject": "...", "extra": {} }`
|
||||
# The request will pass the subject through successfully, otherwise it will be marked as unauthorized
|
||||
check_session_url: https://session-store-host
|
||||
|
||||
# Sets a list of possible cookies to look for on incoming requests, and will fallthrough to the next authenticator if
|
||||
# none of the passed cookies are set on the request
|
||||
only:
|
||||
- sessionid
|
||||
|
||||
# Configures the jwt authenticator
|
||||
jwt:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# REQUIRED IF ENABLED - The URL where ORY Oathkeeper can retrieve JSON Web Keys from for validating the JSON Web
|
||||
# Token. Usually something like "https://my-keys.com/.well-known/jwks.json". The response of that endpoint must
|
||||
# return a JSON Web Key Set (JWKS).
|
||||
jwks_urls:
|
||||
- https://my-website.com/.well-known/jwks.json
|
||||
- https://my-other-website.com/.well-known/jwks.json
|
||||
- file://path/to/local/jwks.json
|
||||
|
||||
# Sets the strategy to be used to validate/match the scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults
|
||||
# to "none".
|
||||
scope_strategy: wildcard
|
||||
|
||||
# Configures the noop authenticator
|
||||
noop:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
# Configures the oauth2_client_credentials authenticator
|
||||
oauth2_client_credentials:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint that will be used to validate the client credentials.
|
||||
token_url: https://my-website.com/oauth2/token
|
||||
|
||||
# Configures the oauth2_introspection authenticator
|
||||
oauth2_introspection:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# REQUIRED IF ENABLED - The OAuth 2.0 Token Introspection endpoint.
|
||||
introspection_url: https://my-website.com/oauth2/introspection
|
||||
|
||||
# Sets the strategy to be used to validate/match the token scope. Supports "hierarchic", "exact", "wildcard", "none". Defaults
|
||||
# to "none".
|
||||
scope_strategy: exact
|
||||
|
||||
# Enable pre-authorization in cases where the OAuth 2.0 Token Introspection endpoint is protected by OAuth 2.0 Bearer
|
||||
# Tokens that can be retrieved using the OAuth 2.0 Client Credentials grant.
|
||||
pre_authorization:
|
||||
# Enable pre-authorization. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
# REQUIRED IF ENABLED - The OAuth 2.0 Client ID to be used for the OAuth 2.0 Client Credentials Grant.
|
||||
client_id: some_id
|
||||
|
||||
# REQUIRED IF ENABLED - The OAuth 2.0 Client Secret to be used for the OAuth 2.0 Client Credentials Grant.
|
||||
client_secret: some_secret
|
||||
|
||||
# The OAuth 2.0 Scope to be requested during the OAuth 2.0 Client Credentials Grant.
|
||||
scope:
|
||||
- foo
|
||||
- bar
|
||||
|
||||
# REQUIRED IF ENABLED - The OAuth 2.0 Token Endpoint where the OAuth 2.0 Client Credentials Grant will be performed.
|
||||
token_url: https://my-website.com/oauth2/token
|
||||
|
||||
# Configures the unauthorized authenticator
|
||||
unauthorized:
|
||||
# Set enabled to true if the authenticator should be enabled and false to disable the authenticator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
# All authorizers can be configured under this configuration key
|
||||
authorizers:
|
||||
# Configures the allow authorizer
|
||||
allow:
|
||||
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
# Configures the deny authorizer
|
||||
deny:
|
||||
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
# Configures the keto_engine_acp_ory authorizer
|
||||
keto_engine_acp_ory:
|
||||
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
# REQUIRED IF ENABLED - The base URL of ORY Keto, typically something like http(s)://<host>[:<port>]/
|
||||
base_url: http://my-keto/
|
||||
required_action: unknown
|
||||
required_resource: unknown
|
||||
|
||||
# Configures the remote authorizer
|
||||
remote:
|
||||
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
remote: https://host/path
|
||||
headers: {}
|
||||
|
||||
# Configures the remote_json authorizer
|
||||
remote_json:
|
||||
# Set enabled to true if the authorizer should be enabled and false to disable the authorizer. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
remote: https://host/path
|
||||
payload: "{}"
|
||||
|
||||
# All mutators can be configured under this configuration key
|
||||
mutators:
|
||||
header:
|
||||
enabled: true
|
||||
config:
|
||||
headers:
|
||||
foo: bar
|
||||
|
||||
# Configures the cookie mutator
|
||||
cookie:
|
||||
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
|
||||
enabled: true
|
||||
config:
|
||||
cookies:
|
||||
foo: bar
|
||||
|
||||
# Configures the hydrator mutator
|
||||
hydrator:
|
||||
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
|
||||
enabled: true
|
||||
|
||||
config:
|
||||
api:
|
||||
url: https://some-url/
|
||||
|
||||
# Configures the id_token mutator
|
||||
id_token:
|
||||
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
|
||||
enabled: true
|
||||
config:
|
||||
# REQUIRED IF ENABLED - Sets the "iss" value of the ID Token.
|
||||
issuer_url: https://my-oathkeeper/
|
||||
# REQUIRED IF ENABLED - Sets the URL where keys should be fetched from. Supports remote locations (http, https) as
|
||||
# well as local filesystem paths.
|
||||
jwks_url: https://fetch-keys/from/this/location.json
|
||||
# jwks_url: file:///from/this/absolute/location.json
|
||||
# jwks_url: file://../from/this/relative/location.json
|
||||
|
||||
# Sets the time-to-live of the ID token. Defaults to one minute. Valid time units are: s (second), m (minute), h (hour).
|
||||
ttl: 1h
|
||||
|
||||
# Configures the noop mutator
|
||||
noop:
|
||||
# Set enabled to true if the mutator should be enabled and false to disable the mutator. Defaults to false.
|
||||
enabled: true
|
||||
204
oryx/configx/stub/benchmark/schema.config.json
Normal file
204
oryx/configx/stub/benchmark/schema.config.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"log": {
|
||||
"level": "debug",
|
||||
"format": "json"
|
||||
},
|
||||
"profiling": "cpu",
|
||||
"serve": {
|
||||
"proxy": {
|
||||
"port": 1234,
|
||||
"host": "127.0.0.1",
|
||||
"timeout": {
|
||||
"read": "1s",
|
||||
"write": "2s",
|
||||
"idle": "3s"
|
||||
},
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"allowed_origins": ["https://example.com", "https://*.example.com"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_headers": ["Authorization", "Content-Type"],
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"allow_credentials": true,
|
||||
"max_age": 10,
|
||||
"debug": true
|
||||
},
|
||||
"tls": {
|
||||
"key": {
|
||||
"path": "/path/to/key.pem",
|
||||
"base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..."
|
||||
},
|
||||
"cert": {
|
||||
"path": "/path/to/cert.pem",
|
||||
"base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..."
|
||||
}
|
||||
}
|
||||
},
|
||||
"api": {
|
||||
"port": 1235,
|
||||
"host": "127.0.0.2",
|
||||
"cors": {
|
||||
"enabled": true,
|
||||
"allowed_origins": ["https://example.org", "https://*.example.org"],
|
||||
"allowed_methods": ["GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_headers": ["Authorization", "Content-Type"],
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"allow_credentials": true,
|
||||
"max_age": 10,
|
||||
"debug": true
|
||||
},
|
||||
"tls": {
|
||||
"key": {
|
||||
"path": "/path/to/key.pem",
|
||||
"base64": "LS0tLS1CRUdJTiBFTkNSWVBURUQgUFJJVkFURSBLRVktLS0tLVxuTUlJRkRqQkFCZ2txaGtpRzl3MEJCUTB3..."
|
||||
},
|
||||
"cert": {
|
||||
"path": "/path/to/cert.pem",
|
||||
"base64": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"access_rules": {
|
||||
"repositories": [
|
||||
"file://path/to/rules.json",
|
||||
"inline://W3siaWQiOiJmb28tcnVsZSIsImF1dGhlbnRpY2F0b3JzIjpbXX1d",
|
||||
"https://path-to-my-rules/rules.json"
|
||||
],
|
||||
"matching_strategy": "glob"
|
||||
},
|
||||
"errors": {
|
||||
"fallback": ["json"],
|
||||
"handlers": {
|
||||
"redirect": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"to": "http://path-to/redirect"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"verbose": true,
|
||||
"when": [
|
||||
{
|
||||
"error": ["unauthorized", "forbidden", "internal_server_error"],
|
||||
"request": {
|
||||
"header": {
|
||||
"content_type": ["application/json"],
|
||||
"accept": ["application/json"]
|
||||
},
|
||||
"cidr": ["127.0.0.0/24"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"authenticators": {
|
||||
"anonymous": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"subject": "guest"
|
||||
}
|
||||
},
|
||||
"cookie_session": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"check_session_url": "https://session-store-host",
|
||||
"only": ["sessionid"]
|
||||
}
|
||||
},
|
||||
"jwt": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"jwks_urls": [
|
||||
"https://my-website.com/.well-known/jwks.json",
|
||||
"https://my-other-website.com/.well-known/jwks.json",
|
||||
"file://path/to/local/jwks.json"
|
||||
],
|
||||
"scope_strategy": "wildcard"
|
||||
}
|
||||
},
|
||||
"noop": {
|
||||
"enabled": true
|
||||
},
|
||||
"oauth2_client_credentials": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"token_url": "https://my-website.com/oauth2/token"
|
||||
}
|
||||
},
|
||||
"oauth2_introspection": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"introspection_url": "https://my-website.com/oauth2/introspection",
|
||||
"scope_strategy": "exact",
|
||||
"pre_authorization": {
|
||||
"enabled": true,
|
||||
"client_id": "some_id",
|
||||
"client_secret": "some_secret",
|
||||
"scope": ["foo", "bar"],
|
||||
"token_url": "https://my-website.com/oauth2/token"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unauthorized": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"authorizers": {
|
||||
"allow": {
|
||||
"enabled": true
|
||||
},
|
||||
"deny": {
|
||||
"enabled": true
|
||||
},
|
||||
"keto_engine_acp_ory": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"base_url": "http://my-keto/",
|
||||
"required_action": "unknown",
|
||||
"required_resource": "unknown"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mutators": {
|
||||
"header": {
|
||||
"enabled": false,
|
||||
"config": {
|
||||
"headers": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cookie": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"cookies": {
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"hydrator": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"api": {
|
||||
"url": "https://some-url/"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id_token": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"issuer_url": "https://my-oathkeeper/",
|
||||
"jwks_url": "https://fetch-keys/from/this/location.json",
|
||||
"ttl": "1h"
|
||||
}
|
||||
},
|
||||
"noop": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
41
oryx/configx/stub/domain-aliases/config.schema.json
Normal file
41
oryx/configx/stub/domain-aliases/config.schema.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"domain_aliases": {
|
||||
"title": "Domain Aliases",
|
||||
"description": "Adds an alias domain. If a request with the hostname (FQDN) matching the hostname in the alias is found, that URL is used as the base URL.",
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"required": ["match_domain", "base_path", "scheme"],
|
||||
"properties": {
|
||||
"match_domain": {
|
||||
"minLength": 1,
|
||||
"title": "Matching Domain",
|
||||
"description": "Sets the matching domain. If the domain matches with this entry, the accompanying base_url will be used.",
|
||||
"type": "string",
|
||||
"examples": ["localhost", "my-domain.com"]
|
||||
},
|
||||
"scheme": {
|
||||
"title": "Scheme",
|
||||
"description": "Sets the scheme, for example https or http.",
|
||||
"type": "string",
|
||||
"enum": ["http", "https"]
|
||||
},
|
||||
"base_path": {
|
||||
"minLength": 1,
|
||||
"title": "Base Path",
|
||||
"description": "Sets the base path for the matched domain.",
|
||||
"type": "string",
|
||||
"default": "/",
|
||||
"pattern": "^/.*$",
|
||||
"examples": ["/", "/.ory/kratos"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
27
oryx/configx/stub/from-files/a.yaml
Normal file
27
oryx/configx/stub/from-files/a.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: v0.5.3-alpha.1
|
||||
|
||||
dsn: memory
|
||||
|
||||
serve:
|
||||
public:
|
||||
base_url: http://127.0.0.1:4433/
|
||||
cors:
|
||||
enabled: true
|
||||
admin:
|
||||
base_url: http://kratos:4434/
|
||||
|
||||
selfservice:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
whitelisted_return_urls:
|
||||
- http://127.0.0.1:4455
|
||||
|
||||
methods:
|
||||
password:
|
||||
enabled: true
|
||||
|
||||
flows:
|
||||
error:
|
||||
ui_url: http://127.0.0.1:4455/error
|
||||
|
||||
settings:
|
||||
ui_url: http://127.0.0.1:4455/settings
|
||||
54
oryx/configx/stub/from-files/b.yaml
Normal file
54
oryx/configx/stub/from-files/b.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
selfservice:
|
||||
flows:
|
||||
settings:
|
||||
privileged_session_max_age: 15m
|
||||
|
||||
recovery:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/recovery
|
||||
|
||||
verification:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/verify
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
|
||||
logout:
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/auth/login
|
||||
|
||||
login:
|
||||
ui_url: http://127.0.0.1:4455/auth/login
|
||||
lifespan: 10m
|
||||
|
||||
registration:
|
||||
lifespan: 10m
|
||||
ui_url: http://127.0.0.1:4455/auth/registration
|
||||
after:
|
||||
password:
|
||||
hooks:
|
||||
- hook: session
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: text
|
||||
leak_sensitive_values: true
|
||||
|
||||
secrets:
|
||||
cookie:
|
||||
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
|
||||
|
||||
hashers:
|
||||
argon2:
|
||||
parallelism: 1
|
||||
memory: 131072
|
||||
iterations: 2
|
||||
salt_length: 16
|
||||
key_length: 16
|
||||
|
||||
identity:
|
||||
default_schema_url: file:///etc/config/kratos/identity.schema.json
|
||||
|
||||
courier:
|
||||
smtp:
|
||||
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
|
||||
1085
oryx/configx/stub/from-files/config.schema.json
Normal file
1085
oryx/configx/stub/from-files/config.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
124
oryx/configx/stub/from-files/expected.json
Normal file
124
oryx/configx/stub/from-files/expected.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"courier": {
|
||||
"smtp": {
|
||||
"connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true",
|
||||
"from_address": "no-reply@ory.kratos.sh"
|
||||
}
|
||||
},
|
||||
"dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true",
|
||||
"hashers": {
|
||||
"argon2": {
|
||||
"iterations": 2,
|
||||
"key_length": 16,
|
||||
"memory": 131072,
|
||||
"parallelism": 1,
|
||||
"salt_length": 16
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"default_schema_url": "file:///etc/config/kratos/identity.schema.json"
|
||||
},
|
||||
"log": {
|
||||
"format": "text",
|
||||
"leak_sensitive_values": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"secrets": {
|
||||
"cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
|
||||
},
|
||||
"selfservice": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/",
|
||||
"flows": {
|
||||
"error": {
|
||||
"ui_url": "http://127.0.0.1:4455/error"
|
||||
},
|
||||
"login": {
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/login"
|
||||
},
|
||||
"logout": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/auth/login"
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/recovery"
|
||||
},
|
||||
"registration": {
|
||||
"after": {
|
||||
"password": {
|
||||
"hooks": [
|
||||
{
|
||||
"hook": "session"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/registration"
|
||||
},
|
||||
"settings": {
|
||||
"lifespan": "1h",
|
||||
"privileged_session_max_age": "15m",
|
||||
"ui_url": "http://127.0.0.1:4455/settings"
|
||||
},
|
||||
"verification": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/"
|
||||
},
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/verify"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"link": {
|
||||
"enabled": true
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": false
|
||||
},
|
||||
"password": {
|
||||
"enabled": true
|
||||
},
|
||||
"profile": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"whitelisted_return_urls": ["http://127.0.0.1:4455"]
|
||||
},
|
||||
"serve": {
|
||||
"admin": {
|
||||
"base_url": "http://kratos:4434/",
|
||||
"host": "0.0.0.0",
|
||||
"port": 4434
|
||||
},
|
||||
"public": {
|
||||
"base_url": "http://127.0.0.1:4433/",
|
||||
"cors": {
|
||||
"allow_credentials": true,
|
||||
"allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_origins": ["*"],
|
||||
"debug": false,
|
||||
"enabled": true,
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"max_age": 0,
|
||||
"options_passthrough": false
|
||||
},
|
||||
"host": "0.0.0.0",
|
||||
"port": 4433
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"persistent": true,
|
||||
"same_site": "Lax"
|
||||
},
|
||||
"lifespan": "24h"
|
||||
},
|
||||
"version": "v0.5.3-alpha.1"
|
||||
}
|
||||
792
oryx/configx/stub/hydra/config.schema.json
Normal file
792
oryx/configx/stub/hydra/config.schema.json
Normal file
@@ -0,0 +1,792 @@
|
||||
{
|
||||
"$id": "https://github.com/ory/hydra/docs/config.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ORY Hydra Configuration",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"http_method": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"POST",
|
||||
"GET",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"TRACE"
|
||||
]
|
||||
},
|
||||
"port_number": {
|
||||
"type": "integer",
|
||||
"description": "The port to listen on.",
|
||||
"minimum": 1,
|
||||
"maximum": 65535
|
||||
},
|
||||
"socket": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Sets the permissions of the unix socket",
|
||||
"properties": {
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"description": "Owner of unix socket. If empty, the owner will be the user running hydra.",
|
||||
"default": ""
|
||||
},
|
||||
"group": {
|
||||
"type": "string",
|
||||
"description": "Group of unix socket. If empty, the group will be the primary group of the user running hydra.",
|
||||
"default": ""
|
||||
},
|
||||
"mode": {
|
||||
"type": "integer",
|
||||
"description": "Mode of unix socket in numeric form",
|
||||
"default": 493,
|
||||
"minimum": 0,
|
||||
"maximum": 511
|
||||
}
|
||||
}
|
||||
},
|
||||
"cors": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures Cross Origin Resource Sharing for public endpoints.",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Sets whether CORS is enabled.",
|
||||
"default": false
|
||||
},
|
||||
"allowed_origins": {
|
||||
"type": "array",
|
||||
"description": "A list of origins a cross-domain request can be executed from. If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*) to replace 0 or more characters (i.e.: http://*.domain.com). Only one wildcard can be used per origin.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"not": {
|
||||
"type": "string",
|
||||
"description": "does match all strings that contain two or more (*)",
|
||||
"pattern": ".*\\*.*\\*.*"
|
||||
},
|
||||
"anyOf": [
|
||||
{
|
||||
"format": "uri"
|
||||
},
|
||||
{
|
||||
"const": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"default": ["*"],
|
||||
"examples": [
|
||||
[
|
||||
"https://example.com",
|
||||
"https://*.example.com",
|
||||
"https://*.foo.example.com"
|
||||
]
|
||||
]
|
||||
},
|
||||
"allowed_methods": {
|
||||
"type": "array",
|
||||
"description": "A list of HTTP methods the user agent is allowed to use with cross-domain requests.",
|
||||
"default": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"POST",
|
||||
"GET",
|
||||
"PUT",
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"CONNECT",
|
||||
"HEAD",
|
||||
"OPTIONS",
|
||||
"TRACE"
|
||||
]
|
||||
}
|
||||
},
|
||||
"allowed_headers": {
|
||||
"type": "array",
|
||||
"description": "A list of non simple headers the client is allowed to use with cross-domain requests.",
|
||||
"default": ["Authorization", "Content-Type"],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"exposed_headers": {
|
||||
"type": "array",
|
||||
"description": "Sets which headers are safe to expose to the API of a CORS API specification.",
|
||||
"default": ["Content-Type"],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"allow_credentials": {
|
||||
"type": "boolean",
|
||||
"description": "Sets whether the request can include user credentials like cookies, HTTP authentication or client side SSL certificates.",
|
||||
"default": true
|
||||
},
|
||||
"options_passthrough": {
|
||||
"type": "boolean",
|
||||
"description": "TODO",
|
||||
"default": false
|
||||
},
|
||||
"max_age": {
|
||||
"type": "integer",
|
||||
"description": "Sets how long (in seconds) the results of a preflight request can be cached. If set to 0, every request is preceded by a preflight request.",
|
||||
"default": 0,
|
||||
"minimum": 0
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "Adds additional log output to debug server side CORS issues.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"pem_file": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "The path to the pem file.",
|
||||
"examples": ["/path/to/file.pem"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["path"]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"base64": {
|
||||
"type": "string",
|
||||
"description": "The base64 encoded string (without padding).",
|
||||
"contentEncoding": "base64",
|
||||
"contentMediaType": "application/x-pem-file",
|
||||
"examples": ["b3J5IGh5ZHJhIGlzIGF3ZXNvbWUK"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["base64"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"duration": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+(ns|us|ms|s|m|h)$",
|
||||
"examples": ["1h"]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"log": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures the logger",
|
||||
"properties": {
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "Sets the log level.",
|
||||
"enum": ["panic", "fatal", "error", "warn", "info", "debug", "trace"],
|
||||
"default": "info"
|
||||
},
|
||||
"leak_sensitive_values": {
|
||||
"type": "boolean",
|
||||
"description": "Logs sensitive values such as cookie and URL parameter.",
|
||||
"default": false
|
||||
},
|
||||
"redaction_text": {
|
||||
"type": "string",
|
||||
"title": "Sensitive log value redaction text",
|
||||
"description": "Text to use, when redacting sensitive log value."
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"description": "Sets the log format.",
|
||||
"enum": ["json", "json_pretty", "text"],
|
||||
"default": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Controls the configuration for the http(s) daemon(s).",
|
||||
"properties": {
|
||||
"public": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Controls the public daemon serving public API endpoints like /oauth2/auth, /oauth2/token, /.well-known/jwks.json",
|
||||
"properties": {
|
||||
"port": {
|
||||
"default": 4444,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/port_number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The interface or unix socket ORY Hydra should listen and handle public API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.",
|
||||
"default": "",
|
||||
"examples": ["localhost"]
|
||||
},
|
||||
"cors": {
|
||||
"$ref": "#/definitions/cors"
|
||||
},
|
||||
"socket": {
|
||||
"$ref": "#/definitions/socket"
|
||||
},
|
||||
"access_log": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Access Log configuration for public server.",
|
||||
"properties": {
|
||||
"disable_for_health": {
|
||||
"type": "boolean",
|
||||
"description": "Disable access log for health endpoints.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"port": {
|
||||
"default": 4445,
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/port_number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
"description": "The interface or unix socket ORY Hydra should listen and handle administrative API requests on. Use the prefix \"unix:\" to specify a path to a unix socket. Leave empty to listen on all interfaces.",
|
||||
"default": "",
|
||||
"examples": ["localhost"]
|
||||
},
|
||||
"cors": {
|
||||
"$ref": "#/definitions/cors"
|
||||
},
|
||||
"socket": {
|
||||
"$ref": "#/definitions/socket"
|
||||
},
|
||||
"access_log": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Access Log configuration for admin server.",
|
||||
"properties": {
|
||||
"disable_for_health": {
|
||||
"type": "boolean",
|
||||
"description": "Disable access log for health endpoints.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tls": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures HTTPS (HTTP over TLS). If configured, the server automatically supports HTTP/2.",
|
||||
"properties": {
|
||||
"key": {
|
||||
"description": "Configures the private key (pem encoded).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pem_file"
|
||||
}
|
||||
]
|
||||
},
|
||||
"cert": {
|
||||
"description": "Configures the private key (pem encoded).",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/pem_file"
|
||||
}
|
||||
]
|
||||
},
|
||||
"allow_termination_from": {
|
||||
"type": "array",
|
||||
"description": "Whitelist one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. Hydra serves http instead of https when this option is set.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$"
|
||||
},
|
||||
{
|
||||
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
|
||||
}
|
||||
],
|
||||
"examples": ["127.0.0.1/32"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"same_site_mode": {
|
||||
"type": "string",
|
||||
"description": "Specify the SameSite mode that cookies should be sent with.",
|
||||
"enum": ["Strict", "Lax", "None"],
|
||||
"default": "None"
|
||||
},
|
||||
"same_site_legacy_workaround": {
|
||||
"type": "boolean",
|
||||
"description": "Some older browser versions don’t work with SameSite=None. This option enables the workaround defined in https://web.dev/samesite-cookie-recipes/ which essentially stores a second cookie without SameSite as a fallback.",
|
||||
"default": false,
|
||||
"examples": [true]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dsn": {
|
||||
"type": "string",
|
||||
"description": "Sets the data source name. This configures the backend where ORY Hydra persists data. If dsn is \"memory\", data will be written to memory and is lost when you restart this instance. ORY Hydra supports popular SQL databases. For more detailed configuration information go to: https://www.ory.sh/docs/hydra/dependencies-environment#sql"
|
||||
},
|
||||
"webfinger": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures ./well-known/ settings.",
|
||||
"properties": {
|
||||
"jwks": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures the /.well-known/jwks.json endpoint.",
|
||||
"properties": {
|
||||
"broadcast_keys": {
|
||||
"type": "array",
|
||||
"description": "A list of JSON Web Keys that should be exposed at that endpoint. This is usually the public key for verifying OpenID Connect ID Tokens. However, you might want to add additional keys here as well.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": ["hydra.openid.id-token"],
|
||||
"examples": ["hydra.jwt.access-token"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc_discovery": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures OpenID Connect Discovery (/.well-known/openid-configuration).",
|
||||
"properties": {
|
||||
"jwks_url": {
|
||||
"type": "string",
|
||||
"description": "Overwrites the JWKS URL",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-service.com/.well-known/jwks.json"]
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string",
|
||||
"description": "Overwrites the OAuth2 Token URL",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-service.com/oauth2/token"]
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string",
|
||||
"description": "Overwrites the OAuth2 Auth URL",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-service.com/oauth2/auth"]
|
||||
},
|
||||
"client_registration_url": {
|
||||
"description": "Sets the OpenID Connect Dynamic Client Registration Endpoint",
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-service.com/clients"]
|
||||
},
|
||||
"supported_claims": {
|
||||
"type": "array",
|
||||
"description": "A list of supported claims to be broadcasted. Claim \"sub\" is always included.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [["email", "username"]]
|
||||
},
|
||||
"supported_scope": {
|
||||
"type": "array",
|
||||
"description": "The scope OAuth 2.0 Clients may request. Scope `offline`, `offline_access`, and `openid` are always included.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [["email", "whatever", "read.photos"]]
|
||||
},
|
||||
"userinfo_url": {
|
||||
"type": "string",
|
||||
"description": "A URL of the userinfo endpoint to be advertised at the OpenID Connect Discovery endpoint /.well-known/openid-configuration. Defaults to ORY Hydra's userinfo endpoint at /userinfo. Set this value if you want to handle this endpoint yourself.",
|
||||
"format": "uri",
|
||||
"examples": ["https://example.org/my-custom-userinfo-endpoint"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures OpenID Connect features.",
|
||||
"properties": {
|
||||
"subject_identifiers": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures the Subject Identifier algorithm. For more information please head over to the documentation: https://www.ory.sh/docs/hydra/advanced#subject-identifier-algorithms",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "array",
|
||||
"description": "A list of algorithms to enable.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["public", "pairwise"]
|
||||
}
|
||||
},
|
||||
"pairwise": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures the pairwise algorithm.",
|
||||
"properties": {
|
||||
"salt": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["salt"]
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"contains": {
|
||||
"const": "pairwise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"required": ["pairwise"]
|
||||
},
|
||||
"else": {
|
||||
"properties": {
|
||||
"pairwise": {
|
||||
"$comment": "This enforces pairwise to not be set if 'enabled' does not contain 'pairwise'",
|
||||
"not": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"enabled": ["public", "pairwise"],
|
||||
"pairwise": {
|
||||
"salt": "some-random-salt"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"dynamic_client_registration": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures OpenID Connect Dynamic Client Registration (exposed as admin endpoints /clients/...).",
|
||||
"properties": {
|
||||
"default_scope": {
|
||||
"type": "array",
|
||||
"description": "The OpenID Connect Dynamic Client Registration specification has no concept of whitelisting OAuth 2.0 Scope. If you want to expose Dynamic Client Registration, you should set the default scope enabled for newly registered clients. Keep in mind that users can overwrite this default by setting the \"scope\" key in the registration payload, effectively disabling the concept of whitelisted scopes.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"examples": [["openid", "offline", "offline_access"]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"urls": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"self": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"issuer": {
|
||||
"type": "string",
|
||||
"description": "This value will be used as the \"issuer\" in access and ID tokens. It must be specified and using HTTPS protocol, unless --dangerous-force-http is set. This should typically be equal to the public value.",
|
||||
"format": "uri",
|
||||
"examples": ["https://localhost:4444/"]
|
||||
},
|
||||
"public": {
|
||||
"type": "string",
|
||||
"description": "This is the base location of the public endpoints of your ORY Hydra installation. This should typically be equal to the issuer value. If left unspecified, it falls back to the issuer value.",
|
||||
"format": "uri",
|
||||
"examples": ["https://localhost:4444/"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"type": "string",
|
||||
"description": "Sets the login endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-login.app/login"]
|
||||
},
|
||||
"consent": {
|
||||
"type": "string",
|
||||
"description": "Sets the consent endpoint of the User Login & Consent flow. Defaults to an internal fallback URL showing an error.",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-consent.app/consent"]
|
||||
},
|
||||
"logout": {
|
||||
"type": "string",
|
||||
"description": "Sets the logout endpoint. Defaults to an internal fallback URL showing an error.",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-logout.app/logout"]
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"description": "Sets the error endpoint. The error ui will be shown when an OAuth2 error occurs that which can not be sent back to the client. Defaults to an internal fallback URL showing an error.",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-error.app/error"]
|
||||
},
|
||||
"post_logout_redirect": {
|
||||
"type": "string",
|
||||
"description": "When a user agent requests to logout, it will be redirected to this url afterwards per default.",
|
||||
"format": "uri",
|
||||
"examples": ["https://my-example.app/logout-successful"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"strategies": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Defines how scopes are matched. For more details have a look at https://github.com/ory/fosite#scopes",
|
||||
"enum": [
|
||||
"exact",
|
||||
"wildcard",
|
||||
"DEPRECATED_HIERARCHICAL_SCOPE_STRATEGY"
|
||||
],
|
||||
"default": "wildcard"
|
||||
},
|
||||
"access_token": {
|
||||
"type": "string",
|
||||
"description": "Defines access token type. jwt is a bad idea, see https://www.ory.sh/docs/hydra/advanced#json-web-tokens",
|
||||
"enum": ["opaque", "jwt"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ttl": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures time to live.",
|
||||
"properties": {
|
||||
"login_consent_request": {
|
||||
"description": "Configures how long a user login and consent flow may take.",
|
||||
"default": "1h",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/duration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"access_token": {
|
||||
"description": "Configures how long access tokens are valid.",
|
||||
"default": "1h",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/duration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"refresh_token": {
|
||||
"description": "Configures how long refresh tokens are valid. Set to -1 for refresh tokens to never expire.",
|
||||
"default": "720h",
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/duration"
|
||||
},
|
||||
{
|
||||
"enum": ["-1", -1]
|
||||
}
|
||||
]
|
||||
},
|
||||
"id_token": {
|
||||
"description": "Configures how long id tokens are valid.",
|
||||
"default": "1h",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/duration"
|
||||
}
|
||||
]
|
||||
},
|
||||
"auth_code": {
|
||||
"description": "Configures how long auth codes are valid.",
|
||||
"default": "10m",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/duration"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"oauth2": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"expose_internal_errors": {
|
||||
"type": "boolean",
|
||||
"description": "Set this to true if you want to share error debugging information with your OAuth 2.0 clients. Keep in mind that debug information is very valuable when dealing with errors, but might also expose database error codes and similar errors.",
|
||||
"default": false,
|
||||
"examples": [true]
|
||||
},
|
||||
"session": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"encrypt_at_rest": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"title": "Encrypt OAuth2 Session",
|
||||
"description": "If set to true (default) ORY Hydra encrypt OAuth2 and OpenID Connect session data using AES-GCM and the system secret before persisting it in the database."
|
||||
}
|
||||
}
|
||||
},
|
||||
"include_legacy_error_fields": {
|
||||
"type": "boolean",
|
||||
"description": "Set this to true if you want to include the `error_hint` and `error_debug` legacy fields in error responses. We recommend to set this to `false` unless you have clients using these fields.",
|
||||
"default": false,
|
||||
"examples": [true]
|
||||
},
|
||||
"hashers": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures hashing algorithms. Supports only BCrypt at the moment.",
|
||||
"properties": {
|
||||
"bcrypt": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures the BCrypt hashing algorithm used for hashing Client Secrets.",
|
||||
"properties": {
|
||||
"cost": {
|
||||
"type": "integer",
|
||||
"description": "Sets the BCrypt cost. The higher the value, the more CPU time is being used to generate hashes.",
|
||||
"default": 10,
|
||||
"minimum": 4,
|
||||
"maximum": 31
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"pkce": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enforced": {
|
||||
"type": "boolean",
|
||||
"description": "Sets whether PKCE should be enforced for all clients.",
|
||||
"examples": [true]
|
||||
},
|
||||
"enforced_for_public_clients": {
|
||||
"type": "boolean",
|
||||
"description": "Sets whether PKCE should be enforced for public clients.",
|
||||
"examples": [true]
|
||||
}
|
||||
}
|
||||
},
|
||||
"client_credentials": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"default_grant_allowed_scope": {
|
||||
"type": "boolean",
|
||||
"description": "Defines how scopes are added if the request doesn't contains any scope",
|
||||
"examples": [false]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "The secrets section configures secrets used for encryption and signing of several systems. All secrets can be rotated, for more information on this topic go to: https://www.ory.sh/docs/hydra/advanced#rotation-of-hmac-token-signing-and-database-and-cookie-encryption-keys",
|
||||
"properties": {
|
||||
"system": {
|
||||
"description": "The system secret must be at least 16 characters long. If none is provided, one will be generated. They key is used to encrypt sensitive data using AES-GCM (256 bit) and validate HMAC signatures. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 16
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"this-is-the-primary-secret",
|
||||
"this-is-an-old-secret",
|
||||
"this-is-another-old-secret"
|
||||
]
|
||||
]
|
||||
},
|
||||
"cookie": {
|
||||
"type": "array",
|
||||
"description": "A secret that is used to encrypt cookie sessions. Defaults to secrets.system. It is recommended to use a separate secret in production. The first item in the list is used for signing and encryption. The whole list is used for verifying signatures and decryption.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 16
|
||||
},
|
||||
"examples": [
|
||||
[
|
||||
"this-is-the-primary-secret",
|
||||
"this-is-an-old-secret",
|
||||
"this-is-another-old-secret"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"profiling": {
|
||||
"type": "string",
|
||||
"description": "Enables profiling if set. For more details on profiling, head over to: https://blog.golang.org/profiling-go-programs",
|
||||
"enum": ["cpu", "mem"],
|
||||
"examples": ["cpu"]
|
||||
},
|
||||
"tracing": {
|
||||
"$ref": "ory://tracing-config"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"title": "The Hydra version this config is written for.",
|
||||
"description": "SemVer according to https://semver.org/ prefixed with `v` as in our releases.",
|
||||
"pattern": "^v(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
|
||||
},
|
||||
"cgroups": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "ORY Hydra can respect Linux container CPU quota",
|
||||
"properties": {
|
||||
"v1": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"description": "Configures parameters using cgroups v1 hierarchy",
|
||||
"properties": {
|
||||
"auto_max_procs_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Set GOMAXPROCS automatically according to cgroups limits",
|
||||
"default": false,
|
||||
"examples": [true]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["dsn"]
|
||||
}
|
||||
122
oryx/configx/stub/hydra/expected.json
Normal file
122
oryx/configx/stub/hydra/expected.json
Normal file
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"cgroups": {
|
||||
"v1": {
|
||||
"auto_max_procs_enabled": false
|
||||
}
|
||||
},
|
||||
"dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true",
|
||||
"log": {
|
||||
"format": "text",
|
||||
"leak_sensitive_values": false,
|
||||
"level": "info"
|
||||
},
|
||||
"oauth2": {
|
||||
"expose_internal_errors": false,
|
||||
"hashers": {
|
||||
"bcrypt": {
|
||||
"cost": 10
|
||||
}
|
||||
},
|
||||
"include_legacy_error_fields": false,
|
||||
"session": {
|
||||
"encrypt_at_rest": true
|
||||
}
|
||||
},
|
||||
"oidc": {
|
||||
"subject_identifiers": {
|
||||
"enabled": ["pairwise", "public"],
|
||||
"pairwise": {
|
||||
"salt": "youReallyNeedToChangeThis"
|
||||
}
|
||||
}
|
||||
},
|
||||
"secrets": {
|
||||
"system": ["youReallyNeedToChangeThis"]
|
||||
},
|
||||
"serve": {
|
||||
"admin": {
|
||||
"access_log": {
|
||||
"disable_for_health": false
|
||||
},
|
||||
"cors": {
|
||||
"allow_credentials": true,
|
||||
"allowed_headers": ["Authorization", "Content-Type"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_origins": ["*"],
|
||||
"debug": false,
|
||||
"enabled": false,
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"max_age": 0,
|
||||
"options_passthrough": false
|
||||
},
|
||||
"host": "",
|
||||
"port": 4445,
|
||||
"socket": {
|
||||
"group": "",
|
||||
"mode": 493,
|
||||
"owner": ""
|
||||
}
|
||||
},
|
||||
"cookies": {
|
||||
"same_site_legacy_workaround": false,
|
||||
"same_site_mode": "Lax"
|
||||
},
|
||||
"public": {
|
||||
"access_log": {
|
||||
"disable_for_health": false
|
||||
},
|
||||
"cors": {
|
||||
"allow_credentials": true,
|
||||
"allowed_headers": ["Authorization", "Content-Type"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_origins": ["*"],
|
||||
"debug": false,
|
||||
"enabled": false,
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"max_age": 0,
|
||||
"options_passthrough": false
|
||||
},
|
||||
"host": "",
|
||||
"port": 4444,
|
||||
"socket": {
|
||||
"group": "",
|
||||
"mode": 493,
|
||||
"owner": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"strategies": {
|
||||
"scope": "wildcard"
|
||||
},
|
||||
"tracing": {
|
||||
"provider": "jaeger",
|
||||
"providers": {
|
||||
"jaeger": {
|
||||
"local_agent_address": "jaeger:6831",
|
||||
"sampling": {
|
||||
"server_url": "http://jaeger:5778/sampling"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ttl": {
|
||||
"access_token": "1h",
|
||||
"auth_code": "10m",
|
||||
"id_token": "1h",
|
||||
"login_consent_request": "1h",
|
||||
"refresh_token": "720h"
|
||||
},
|
||||
"urls": {
|
||||
"consent": "http://127.0.0.1:3000/consent",
|
||||
"login": "http://127.0.0.1:3000/login",
|
||||
"logout": "http://127.0.0.1:3000/logout",
|
||||
"self": {
|
||||
"issuer": "http://127.0.0.1:4444"
|
||||
}
|
||||
},
|
||||
"webfinger": {
|
||||
"jwks": {
|
||||
"broadcast_keys": ["hydra.openid.id-token"]
|
||||
}
|
||||
}
|
||||
}
|
||||
22
oryx/configx/stub/hydra/hydra.yaml
Normal file
22
oryx/configx/stub/hydra/hydra.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
serve:
|
||||
cookies:
|
||||
same_site_mode: Lax
|
||||
|
||||
urls:
|
||||
self:
|
||||
issuer: http://127.0.0.1:4444
|
||||
consent: http://127.0.0.1:3000/consent
|
||||
login: http://127.0.0.1:3000/login
|
||||
logout: http://127.0.0.1:3000/logout
|
||||
|
||||
secrets:
|
||||
system:
|
||||
- youReallyNeedToChangeThis
|
||||
|
||||
oidc:
|
||||
subject_identifiers:
|
||||
enabled:
|
||||
- pairwise
|
||||
- public
|
||||
pairwise:
|
||||
salt: youReallyNeedToChangeThis
|
||||
1085
oryx/configx/stub/kratos/config.schema.json
Normal file
1085
oryx/configx/stub/kratos/config.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
135
oryx/configx/stub/kratos/expected.json
Normal file
135
oryx/configx/stub/kratos/expected.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"courier": {
|
||||
"smtp": {
|
||||
"connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true",
|
||||
"from_address": "no-reply@ory.kratos.sh"
|
||||
}
|
||||
},
|
||||
"dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true",
|
||||
"hashers": {
|
||||
"argon2": {
|
||||
"iterations": 2,
|
||||
"key_length": 16,
|
||||
"memory": 131072,
|
||||
"parallelism": 1,
|
||||
"salt_length": 16
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"default_schema_url": "file:///etc/config/kratos/identity.schema.json"
|
||||
},
|
||||
"log": {
|
||||
"format": "text",
|
||||
"leak_sensitive_values": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"secrets": {
|
||||
"cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
|
||||
},
|
||||
"selfservice": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/",
|
||||
"flows": {
|
||||
"error": {
|
||||
"ui_url": "http://127.0.0.1:4455/error"
|
||||
},
|
||||
"login": {
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/login"
|
||||
},
|
||||
"logout": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/auth/login"
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/recovery"
|
||||
},
|
||||
"registration": {
|
||||
"after": {
|
||||
"password": {
|
||||
"hooks": [
|
||||
{
|
||||
"hook": "session"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/registration"
|
||||
},
|
||||
"settings": {
|
||||
"lifespan": "1h",
|
||||
"privileged_session_max_age": "15m",
|
||||
"ui_url": "http://127.0.0.1:4455/settings"
|
||||
},
|
||||
"verification": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/"
|
||||
},
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/verify"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"link": {
|
||||
"enabled": true
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"providers": [
|
||||
{
|
||||
"id": "google",
|
||||
"provider": "google",
|
||||
"mapper_url": "file:///etc/config/kratos/oidc.google.jsonnet",
|
||||
"client_id": "client@example.com",
|
||||
"client_secret": "secret"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"password": {
|
||||
"enabled": true
|
||||
},
|
||||
"profile": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"whitelisted_return_urls": ["http://127.0.0.1:4455"]
|
||||
},
|
||||
"serve": {
|
||||
"admin": {
|
||||
"base_url": "http://kratos:4434/",
|
||||
"host": "0.0.0.0",
|
||||
"port": 4434
|
||||
},
|
||||
"public": {
|
||||
"base_url": "http://127.0.0.1:4433/",
|
||||
"cors": {
|
||||
"allow_credentials": true,
|
||||
"allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_origins": ["*"],
|
||||
"debug": false,
|
||||
"enabled": true,
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"max_age": 0,
|
||||
"options_passthrough": false
|
||||
},
|
||||
"host": "0.0.0.0",
|
||||
"port": 4433
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"persistent": true,
|
||||
"same_site": "Lax"
|
||||
},
|
||||
"lifespan": "24h"
|
||||
},
|
||||
"version": "v0.5.3-alpha.1"
|
||||
}
|
||||
76
oryx/configx/stub/kratos/kratos.yaml
Normal file
76
oryx/configx/stub/kratos/kratos.yaml
Normal file
@@ -0,0 +1,76 @@
|
||||
version: v0.5.3-alpha.1
|
||||
|
||||
dsn: memory
|
||||
|
||||
serve:
|
||||
public:
|
||||
base_url: http://127.0.0.1:4433/
|
||||
cors:
|
||||
enabled: true
|
||||
admin:
|
||||
base_url: http://kratos:4434/
|
||||
|
||||
selfservice:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
whitelisted_return_urls:
|
||||
- http://127.0.0.1:4455
|
||||
|
||||
methods:
|
||||
password:
|
||||
enabled: true
|
||||
oidc:
|
||||
enabled: true
|
||||
|
||||
flows:
|
||||
error:
|
||||
ui_url: http://127.0.0.1:4455/error
|
||||
|
||||
settings:
|
||||
ui_url: http://127.0.0.1:4455/settings
|
||||
privileged_session_max_age: 15m
|
||||
|
||||
recovery:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/recovery
|
||||
|
||||
verification:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/verify
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
|
||||
logout:
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/auth/login
|
||||
|
||||
login:
|
||||
ui_url: http://127.0.0.1:4455/auth/login
|
||||
lifespan: 10m
|
||||
|
||||
registration:
|
||||
lifespan: 10m
|
||||
ui_url: http://127.0.0.1:4455/auth/registration
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: text
|
||||
leak_sensitive_values: true
|
||||
|
||||
secrets:
|
||||
cookie:
|
||||
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
|
||||
|
||||
hashers:
|
||||
argon2:
|
||||
parallelism: 1
|
||||
memory: 131072
|
||||
iterations: 2
|
||||
salt_length: 16
|
||||
key_length: 16
|
||||
|
||||
identity:
|
||||
default_schema_url: file:///etc/config/kratos/identity.schema.json
|
||||
|
||||
courier:
|
||||
smtp:
|
||||
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
|
||||
27
oryx/configx/stub/multi/a.yaml
Normal file
27
oryx/configx/stub/multi/a.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
version: v0.5.3-alpha.1
|
||||
|
||||
dsn: memory
|
||||
|
||||
serve:
|
||||
public:
|
||||
base_url: http://127.0.0.1:4433/
|
||||
cors:
|
||||
enabled: true
|
||||
admin:
|
||||
base_url: http://kratos:4434/
|
||||
|
||||
selfservice:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
whitelisted_return_urls:
|
||||
- http://127.0.0.1:4455
|
||||
|
||||
methods:
|
||||
password:
|
||||
enabled: true
|
||||
|
||||
flows:
|
||||
error:
|
||||
ui_url: http://127.0.0.1:4455/error
|
||||
|
||||
settings:
|
||||
ui_url: http://127.0.0.1:4455/settings
|
||||
54
oryx/configx/stub/multi/b.yaml
Normal file
54
oryx/configx/stub/multi/b.yaml
Normal file
@@ -0,0 +1,54 @@
|
||||
selfservice:
|
||||
flows:
|
||||
settings:
|
||||
privileged_session_max_age: 15m
|
||||
|
||||
recovery:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/recovery
|
||||
|
||||
verification:
|
||||
enabled: true
|
||||
ui_url: http://127.0.0.1:4455/verify
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/
|
||||
|
||||
logout:
|
||||
after:
|
||||
default_browser_return_url: http://127.0.0.1:4455/auth/login
|
||||
|
||||
login:
|
||||
ui_url: http://127.0.0.1:4455/auth/login
|
||||
lifespan: 10m
|
||||
|
||||
registration:
|
||||
lifespan: 10m
|
||||
ui_url: http://127.0.0.1:4455/auth/registration
|
||||
after:
|
||||
password:
|
||||
hooks:
|
||||
- hook: session
|
||||
|
||||
log:
|
||||
level: debug
|
||||
format: text
|
||||
leak_sensitive_values: true
|
||||
|
||||
secrets:
|
||||
cookie:
|
||||
- PLEASE-CHANGE-ME-I-AM-VERY-INSECURE
|
||||
|
||||
hashers:
|
||||
argon2:
|
||||
parallelism: 1
|
||||
memory: 131072
|
||||
iterations: 2
|
||||
salt_length: 16
|
||||
key_length: 16
|
||||
|
||||
identity:
|
||||
default_schema_url: file:///etc/config/kratos/identity.schema.json
|
||||
|
||||
courier:
|
||||
smtp:
|
||||
connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true
|
||||
1085
oryx/configx/stub/multi/config.schema.json
Normal file
1085
oryx/configx/stub/multi/config.schema.json
Normal file
File diff suppressed because it is too large
Load Diff
124
oryx/configx/stub/multi/expected.json
Normal file
124
oryx/configx/stub/multi/expected.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"courier": {
|
||||
"smtp": {
|
||||
"connection_uri": "smtps://test:test@mailslurper:1025/?skip_ssl_verify=true",
|
||||
"from_address": "no-reply@ory.kratos.sh"
|
||||
}
|
||||
},
|
||||
"dsn": "sqlite:///var/lib/sqlite/db.sqlite?_fk=true",
|
||||
"hashers": {
|
||||
"argon2": {
|
||||
"iterations": 2,
|
||||
"key_length": 16,
|
||||
"memory": 131072,
|
||||
"parallelism": 1,
|
||||
"salt_length": 16
|
||||
}
|
||||
},
|
||||
"identity": {
|
||||
"default_schema_url": "file:///etc/config/kratos/identity.schema.json"
|
||||
},
|
||||
"log": {
|
||||
"format": "text",
|
||||
"leak_sensitive_values": true,
|
||||
"level": "debug"
|
||||
},
|
||||
"secrets": {
|
||||
"cookie": ["PLEASE-CHANGE-ME-I-AM-VERY-INSECURE"]
|
||||
},
|
||||
"selfservice": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/",
|
||||
"flows": {
|
||||
"error": {
|
||||
"ui_url": "http://127.0.0.1:4455/error"
|
||||
},
|
||||
"login": {
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/login"
|
||||
},
|
||||
"logout": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/auth/login"
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/recovery"
|
||||
},
|
||||
"registration": {
|
||||
"after": {
|
||||
"password": {
|
||||
"hooks": [
|
||||
{
|
||||
"hook": "session"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"lifespan": "10m",
|
||||
"ui_url": "http://127.0.0.1:4455/auth/registration"
|
||||
},
|
||||
"settings": {
|
||||
"lifespan": "1h",
|
||||
"privileged_session_max_age": "15m",
|
||||
"ui_url": "http://127.0.0.1:4455/settings"
|
||||
},
|
||||
"verification": {
|
||||
"after": {
|
||||
"default_browser_return_url": "http://127.0.0.1:4455/"
|
||||
},
|
||||
"enabled": true,
|
||||
"lifespan": "1h",
|
||||
"ui_url": "http://127.0.0.1:4455/verify"
|
||||
}
|
||||
},
|
||||
"methods": {
|
||||
"link": {
|
||||
"enabled": true
|
||||
},
|
||||
"oidc": {
|
||||
"enabled": false
|
||||
},
|
||||
"password": {
|
||||
"enabled": true
|
||||
},
|
||||
"profile": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"whitelisted_return_urls": ["http://127.0.0.1:4455"]
|
||||
},
|
||||
"serve": {
|
||||
"admin": {
|
||||
"base_url": "http://kratos:4434/",
|
||||
"host": "0.0.0.0",
|
||||
"port": 4434
|
||||
},
|
||||
"public": {
|
||||
"base_url": "http://127.0.0.1:4433/",
|
||||
"cors": {
|
||||
"allow_credentials": true,
|
||||
"allowed_headers": ["Authorization", "Content-Type", "X-Session-Token"],
|
||||
"allowed_methods": ["POST", "GET", "PUT", "PATCH", "DELETE"],
|
||||
"allowed_origins": ["*"],
|
||||
"debug": false,
|
||||
"enabled": true,
|
||||
"exposed_headers": ["Content-Type"],
|
||||
"max_age": 0,
|
||||
"options_passthrough": false
|
||||
},
|
||||
"host": "0.0.0.0",
|
||||
"port": 4433
|
||||
}
|
||||
},
|
||||
"session": {
|
||||
"cookie": {
|
||||
"path": "/",
|
||||
"persistent": true,
|
||||
"same_site": "Lax"
|
||||
},
|
||||
"lifespan": "24h"
|
||||
},
|
||||
"version": "v0.5.3-alpha.1"
|
||||
}
|
||||
105
oryx/configx/stub/nested-array/config.schema.json
Normal file
105
oryx/configx/stub/nested-array/config.schema.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"providers": {
|
||||
"title": "OpenID Connect and OAuth2 Providers",
|
||||
"description": "A list and configuration of OAuth2 and OpenID Connect providers ORY Kratos should integrate with.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"examples": ["google"]
|
||||
},
|
||||
"provider": {
|
||||
"title": "Provider",
|
||||
"description": "Can be one of github, gitlab, generic, google, microsoft, discord.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"github",
|
||||
"gitlab",
|
||||
"generic",
|
||||
"google",
|
||||
"microsoft",
|
||||
"discord"
|
||||
],
|
||||
"examples": ["google"]
|
||||
},
|
||||
"client_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"client_secret": {
|
||||
"type": "string"
|
||||
},
|
||||
"issuer_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": ["https://accounts.google.com"]
|
||||
},
|
||||
"auth_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": ["https://accounts.google.com/o/oauth2/v2/auth"]
|
||||
},
|
||||
"token_url": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": ["https://www.googleapis.com/oauth2/v4/token"]
|
||||
},
|
||||
"mapper_url": {
|
||||
"title": "Jsonnet Mapper URL",
|
||||
"description": "The URL where the jsonnet source is located for mapping the provider's data to ORY Kratos data.",
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"examples": [
|
||||
"file://path/to/oidc.jsonnet",
|
||||
"https://foo.bar.com/path/to/oidc.jsonnet",
|
||||
"base64://bG9jYWwgc3ViamVjdCA9I..."
|
||||
]
|
||||
},
|
||||
"scope": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"examples": ["offline_access", "profile"]
|
||||
}
|
||||
},
|
||||
"tenant": {
|
||||
"title": "Azure AD Tenant",
|
||||
"description": "The Azure AD Tenant to use for authentication.",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"common",
|
||||
"organizations",
|
||||
"consumers",
|
||||
"8eaef023-2b34-4da1-9baa-8bc8c9d6a490",
|
||||
"contoso.onmicrosoft.com"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [],
|
||||
"if": {
|
||||
"properties": {
|
||||
"provider": {
|
||||
"const": "microsoft"
|
||||
}
|
||||
},
|
||||
"required": ["provider"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["tenant"]
|
||||
},
|
||||
"else": {
|
||||
"not": {
|
||||
"properties": {
|
||||
"tenant": {}
|
||||
},
|
||||
"required": ["tenant"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
oryx/configx/stub/nested-array/expected.json
Normal file
11
oryx/configx/stub/nested-array/expected.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"providers": [
|
||||
{
|
||||
"id": "google",
|
||||
"client_id": "client@example.com"
|
||||
},
|
||||
{
|
||||
"client_id": "some@example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
2
oryx/configx/stub/nested-array/kratos.yaml
Normal file
2
oryx/configx/stub/nested-array/kratos.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
providers:
|
||||
- id: google
|
||||
19
oryx/configx/stub/watch/config.schema.json
Normal file
19
oryx/configx/stub/watch/config.schema.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$id": "https://example.com/config.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "config",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"dsn": {
|
||||
"type": "string"
|
||||
},
|
||||
"foo": {
|
||||
"const": "bar"
|
||||
},
|
||||
"bar": {
|
||||
"type": "string",
|
||||
"enum": ["foo", "bar", "baz"]
|
||||
}
|
||||
},
|
||||
"required": ["dsn"]
|
||||
}
|
||||
68
oryx/configx/tls.schema.json
Normal file
68
oryx/configx/tls.schema.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$id": "https://github.com/ory/x/tlsx/config.schema.json",
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "HTTPS",
|
||||
"description": "Configure HTTP over TLS (HTTPS).",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"title": "Private Key (PEM)",
|
||||
"$ref": "#/definitions/source"
|
||||
},
|
||||
"cert": {
|
||||
"title": "TLS Certificate (PEM)",
|
||||
"$ref": "#/definitions/source"
|
||||
},
|
||||
"allow_termination_from": {
|
||||
"type": "array",
|
||||
"description": "Allow-list one or multiple CIDR address ranges and allow them to terminate TLS connections. Be aware that the X-Forwarded-Proto header must be set and must never be modifiable by anyone but your proxy / gateway / load balancer. Supports ipv4 and ipv6. The service serves http instead of https when this option is set.",
|
||||
"items": {
|
||||
"description": "CIDR address range.",
|
||||
"type": "string",
|
||||
"oneOf": [
|
||||
{
|
||||
"pattern": "^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$"
|
||||
},
|
||||
{
|
||||
"pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$"
|
||||
}
|
||||
],
|
||||
"examples": ["127.0.0.1/32"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"source": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"path": {
|
||||
"title": "Path to PEM-encoded File",
|
||||
"type": "string",
|
||||
"examples": ["path/to/file.pem"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"base64": {
|
||||
"title": "Base64 Encoded Inline",
|
||||
"description": "The base64 string of the PEM-encoded file content. Can be generated using for example `base64 -i path/to/file.pem`.",
|
||||
"type": "string",
|
||||
"examples": [
|
||||
"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tXG5NSUlEWlRDQ0FrMmdBd0lCQWdJRVY1eE90REFOQmdr..."
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
43
oryx/contextx/contextual.go
Normal file
43
oryx/contextx/contextual.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ory/x/configx"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
type (
|
||||
Contextualizer interface {
|
||||
// Network returns the network id for the given context.
|
||||
Network(ctx context.Context, network uuid.UUID) uuid.UUID
|
||||
|
||||
// Config returns the config for the given context.
|
||||
Config(ctx context.Context, config *configx.Provider) *configx.Provider
|
||||
}
|
||||
Provider interface {
|
||||
Contextualizer() Contextualizer
|
||||
}
|
||||
Static struct {
|
||||
NID uuid.UUID
|
||||
C *configx.Provider
|
||||
}
|
||||
)
|
||||
|
||||
func (d *Static) Network(_ context.Context, nid uuid.UUID) uuid.UUID {
|
||||
if d.NID == uuid.Nil {
|
||||
return nid
|
||||
}
|
||||
return d.NID
|
||||
}
|
||||
|
||||
func (d *Static) Config(_ context.Context, c *configx.Provider) *configx.Provider {
|
||||
if d.C == nil {
|
||||
return c
|
||||
}
|
||||
return d.C
|
||||
}
|
||||
39
oryx/contextx/contextual_mock.go
Normal file
39
oryx/contextx/contextual_mock.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ory/x/configx"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// TestContextualizer is a mock implementation of the Contextualizer interface.
|
||||
type TestContextualizer struct{}
|
||||
|
||||
type contextKeyFake int
|
||||
|
||||
// fakeNIDContext is a test key for NID.
|
||||
const fakeNIDContext contextKeyFake = 1
|
||||
|
||||
// SetNIDContext sets the nid for the given context.
|
||||
func SetNIDContext(ctx context.Context, nid uuid.UUID) context.Context {
|
||||
return context.WithValue(ctx, fakeNIDContext, nid) //nolint:staticcheck
|
||||
}
|
||||
|
||||
// Network returns the network id for the given context.
|
||||
func (d *TestContextualizer) Network(ctx context.Context, network uuid.UUID) uuid.UUID {
|
||||
nid, ok := ctx.Value(fakeNIDContext).(uuid.UUID)
|
||||
if !ok {
|
||||
return network
|
||||
}
|
||||
return nid
|
||||
}
|
||||
|
||||
// Config returns the config for the given context.
|
||||
func (d *TestContextualizer) Config(ctx context.Context, config *configx.Provider) *configx.Provider {
|
||||
return config
|
||||
}
|
||||
27
oryx/contextx/default.go
Normal file
27
oryx/contextx/default.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ory/x/configx"
|
||||
)
|
||||
|
||||
type Default struct{}
|
||||
|
||||
var _ Contextualizer = (*Default)(nil)
|
||||
|
||||
func (d *Default) Network(_ context.Context, network uuid.UUID) uuid.UUID {
|
||||
if network == uuid.Nil {
|
||||
panic("nid must be not nil")
|
||||
}
|
||||
return network
|
||||
}
|
||||
|
||||
func (d *Default) Config(_ context.Context, config *configx.Provider) *configx.Provider {
|
||||
return config
|
||||
}
|
||||
158
oryx/contextx/testhelpers.go
Normal file
158
oryx/contextx/testhelpers.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright © 2024 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
|
||||
"github.com/ory/x/configx"
|
||||
)
|
||||
|
||||
type (
|
||||
TestConfigProvider struct {
|
||||
ConfigSchema []byte
|
||||
Options []configx.OptionModifier
|
||||
}
|
||||
contextKey int
|
||||
)
|
||||
|
||||
func NewTestConfigProvider(schema []byte, opts ...configx.OptionModifier) *TestConfigProvider {
|
||||
return &TestConfigProvider{
|
||||
ConfigSchema: schema,
|
||||
Options: opts,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TestConfigProvider) Network(ctx context.Context, network uuid.UUID) uuid.UUID {
|
||||
return (&Default{}).Network(ctx, network)
|
||||
}
|
||||
|
||||
func (t *TestConfigProvider) Config(ctx context.Context, config *configx.Provider) *configx.Provider {
|
||||
values, ok := ctx.Value(contextConfigKey).([]map[string]any)
|
||||
if !ok {
|
||||
return config
|
||||
}
|
||||
|
||||
opts := make([]configx.OptionModifier, 1, 1+len(values))
|
||||
opts[0] = configx.WithValues(config.All())
|
||||
for _, v := range values {
|
||||
opts = append(opts, configx.WithValues(v))
|
||||
}
|
||||
config, err := configx.New(ctx, t.ConfigSchema, append(t.Options, opts...)...)
|
||||
if err != nil {
|
||||
// This is not production code. The provider is only used in tests.
|
||||
panic(err)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
const contextConfigKey contextKey = 1
|
||||
|
||||
var (
|
||||
_ Contextualizer = (*TestConfigProvider)(nil)
|
||||
)
|
||||
|
||||
func WithConfigValue(ctx context.Context, key string, value any) context.Context {
|
||||
return WithConfigValues(ctx, map[string]any{key: value})
|
||||
}
|
||||
|
||||
func WithConfigValues(ctx context.Context, setValues ...map[string]any) context.Context {
|
||||
values, ok := ctx.Value(contextConfigKey).([]map[string]any)
|
||||
if !ok {
|
||||
values = make([]map[string]any, 0)
|
||||
}
|
||||
newValues := make([]map[string]any, len(values), len(values)+len(setValues))
|
||||
copy(newValues, values)
|
||||
newValues = append(newValues, setValues...)
|
||||
|
||||
return context.WithValue(ctx, contextConfigKey, newValues)
|
||||
}
|
||||
|
||||
type ConfigurableTestHandler struct {
|
||||
configs map[uuid.UUID][]map[string]any
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
func NewConfigurableTestHandler(h http.Handler) *ConfigurableTestHandler {
|
||||
return &ConfigurableTestHandler{
|
||||
configs: make(map[uuid.UUID][]map[string]any),
|
||||
handler: h,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
cID := r.Header.Get("Test-Config-Id")
|
||||
if config, ok := t.configs[uuid.FromStringOrNil(cID)]; ok {
|
||||
r = r.WithContext(WithConfigValues(r.Context(), config...))
|
||||
}
|
||||
t.handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestHandler) RegisterConfig(config ...map[string]any) uuid.UUID {
|
||||
id := uuid.Must(uuid.NewV4())
|
||||
t.configs[id] = config
|
||||
return id
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestHandler) UseConfig(r *http.Request, id uuid.UUID) *http.Request {
|
||||
r.Header.Set("Test-Config-Id", id.String())
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestHandler) UseConfigValues(r *http.Request, values ...map[string]any) *http.Request {
|
||||
return t.UseConfig(r, t.RegisterConfig(values...))
|
||||
}
|
||||
|
||||
type ConfigurableTestServer struct {
|
||||
*httptest.Server
|
||||
handler *ConfigurableTestHandler
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
func NewConfigurableTestServer(h http.Handler) *ConfigurableTestServer {
|
||||
handler := NewConfigurableTestHandler(h)
|
||||
server := httptest.NewServer(handler)
|
||||
|
||||
t := server.Client().Transport
|
||||
cts := &ConfigurableTestServer{
|
||||
handler: handler,
|
||||
Server: server,
|
||||
transport: t,
|
||||
}
|
||||
server.Client().Transport = cts
|
||||
return cts
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestServer) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
config, ok := r.Context().Value(contextConfigKey).([]map[string]any)
|
||||
if ok && config != nil {
|
||||
r = t.handler.UseConfigValues(r, config...)
|
||||
}
|
||||
return t.transport.RoundTrip(r)
|
||||
}
|
||||
|
||||
type AutoContextClient struct {
|
||||
*http.Client
|
||||
transport http.RoundTripper
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (t *ConfigurableTestServer) Client(ctx context.Context) *AutoContextClient {
|
||||
baseClient := *t.Server.Client()
|
||||
autoClient := &AutoContextClient{
|
||||
Client: &baseClient,
|
||||
transport: t,
|
||||
ctx: ctx,
|
||||
}
|
||||
baseClient.Transport = autoClient
|
||||
return autoClient
|
||||
}
|
||||
|
||||
func (c *AutoContextClient) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return c.transport.RoundTrip(r.WithContext(c.ctx))
|
||||
}
|
||||
26
oryx/contextx/tree.go
Normal file
26
oryx/contextx/tree.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package contextx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type ContextKey int
|
||||
|
||||
const (
|
||||
ValidContextKey ContextKey = iota + 1
|
||||
)
|
||||
|
||||
var RootContext = context.WithValue(context.Background(), ValidContextKey, true)
|
||||
|
||||
func TestRootContext(t testing.TB) context.Context {
|
||||
return context.WithValue(t.Context(), ValidContextKey, true)
|
||||
}
|
||||
|
||||
func IsRootContext(ctx context.Context) bool {
|
||||
is, ok := ctx.Value(ValidContextKey).(bool)
|
||||
return is && ok
|
||||
}
|
||||
266
oryx/cookiex/cookiex.go
Normal file
266
oryx/cookiex/cookiex.go
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package cookiex encodes authenticated, encrypted HTTP cookies. Values are
|
||||
// sealed with the Ory-wide AEAD (see ory/x/aead) under keys derived from the
|
||||
// configured secrets with HKDF-SHA256, and bound to their purpose and cookie
|
||||
// name. The package replaces the gorilla/securecookie-based cookie stores; a
|
||||
// decode-only fallback reads cookies minted by those stores until they have
|
||||
// expired.
|
||||
package cookiex
|
||||
|
||||
import (
|
||||
"crypto/hkdf"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/ory/x/aead"
|
||||
)
|
||||
|
||||
const (
|
||||
// formatPrefix marks the versioned wire format. A dot is not part of the
|
||||
// base64url alphabet, so legacy securecookie values can never collide
|
||||
// with the prefix.
|
||||
formatPrefix = "v1."
|
||||
// kdfInfo domain-separates the HKDF key derivation.
|
||||
kdfInfo = "ory/x/cookiex/v1"
|
||||
// aadPrefix domain-separates the additional authenticated data.
|
||||
aadPrefix = "ory/x/cookiex/v1"
|
||||
// defaultMaxAge matches the gorilla/securecookie default that the
|
||||
// previous cookie stores relied on.
|
||||
defaultMaxAge = 30 * 24 * time.Hour
|
||||
// maxCookieValueLength is the browser limit that securecookie also
|
||||
// enforced.
|
||||
maxCookieValueLength = 4096
|
||||
)
|
||||
|
||||
// ErrInvalidCookie is returned when a cookie with the requested name is
|
||||
// present but none of its values could be decoded (or none matched).
|
||||
var ErrInvalidCookie = errors.New("cookiex: cookie could not be decoded")
|
||||
|
||||
type (
|
||||
// Option configures a Codec.
|
||||
Option func(*config)
|
||||
|
||||
config struct {
|
||||
maxAge time.Duration
|
||||
legacyKeyPairs [][]byte
|
||||
legacyEncode bool
|
||||
}
|
||||
)
|
||||
|
||||
// WithMaxAge overrides how old a cookie may be before decoding rejects it.
|
||||
// The default is 30 days; zero disables the check. The value must not be
|
||||
// negative.
|
||||
func WithMaxAge(d time.Duration) Option {
|
||||
return func(c *config) { c.maxAge = d }
|
||||
}
|
||||
|
||||
// Codec seals and opens cookie values of type T. T is serialized as JSON.
|
||||
// A Codec is safe for concurrent use.
|
||||
type Codec[T any] struct {
|
||||
purpose string
|
||||
keys [][32]byte
|
||||
maxAge time.Duration
|
||||
legacy legacyState
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// New returns a codec for the given purpose. The purpose is bound into the
|
||||
// ciphertext and used as the metric label; it must be a short constant like
|
||||
// "kratos/session". Because the purpose is embedded in the additional
|
||||
// authenticated data, it must be non-empty and must not contain a pipe
|
||||
// character. The codec seals with a key derived from the first secret
|
||||
// and opens with keys derived from any of them, so secrets rotate by
|
||||
// prepending a new one.
|
||||
func New[T any](purpose string, secrets [][]byte, opts ...Option) (*Codec[T], error) {
|
||||
if purpose == "" || strings.Contains(purpose, "|") {
|
||||
return nil, errors.New("cookiex: purpose must be non-empty and must not contain a pipe character")
|
||||
}
|
||||
if len(secrets) == 0 {
|
||||
return nil, errors.New("cookiex: at least one secret is required")
|
||||
}
|
||||
cfg := config{maxAge: defaultMaxAge}
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
if cfg.maxAge < 0 {
|
||||
return nil, errors.New("cookiex: max age must not be negative")
|
||||
}
|
||||
if cfg.legacyEncode && len(cfg.legacyKeyPairs) == 0 {
|
||||
return nil, errors.New("cookiex: legacy encode requires legacy key pairs")
|
||||
}
|
||||
keys := make([][32]byte, len(secrets))
|
||||
for i, secret := range secrets {
|
||||
key, err := hkdf.Key(sha256.New, secret, nil, kdfInfo, 32)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cookiex: cannot derive key")
|
||||
}
|
||||
keys[i] = [32]byte(key)
|
||||
}
|
||||
return &Codec[T]{
|
||||
purpose: purpose,
|
||||
keys: keys,
|
||||
maxAge: cfg.maxAge,
|
||||
legacy: newLegacyState(cfg, cfg.maxAge),
|
||||
now: time.Now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// envelope wraps the JSON payload with the seal time so decoding can enforce
|
||||
// the max age.
|
||||
type envelope struct {
|
||||
IssuedAt int64 `json:"iat"`
|
||||
Values json.RawMessage `json:"v"`
|
||||
}
|
||||
|
||||
// aad binds a ciphertext to this codec's purpose and the cookie name, so a
|
||||
// sealed value cannot be replayed as a different cookie or in a different
|
||||
// context, even under the same key.
|
||||
func (c *Codec[T]) aad(name string) []byte {
|
||||
return []byte(aadPrefix + "|" + c.purpose + "|" + name)
|
||||
}
|
||||
|
||||
func (c *Codec[T]) seal(name string, value T) (string, error) {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot marshal cookie value")
|
||||
}
|
||||
plaintext, err := json.Marshal(envelope{IssuedAt: c.now().Unix(), Values: payload})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot marshal envelope")
|
||||
}
|
||||
a, err := aead.New(c.keys[0])
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot create AEAD")
|
||||
}
|
||||
// The nonce is prepended to the ciphertext. AEADs that manage the nonce
|
||||
// internally report a nonce size of zero, so this also covers them.
|
||||
nonce := make([]byte, a.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot generate nonce")
|
||||
}
|
||||
sealed := a.Seal(nonce, nonce, plaintext, c.aad(name))
|
||||
return formatPrefix + base64.RawURLEncoding.EncodeToString(sealed), nil
|
||||
}
|
||||
|
||||
func (c *Codec[T]) open(name, value string) (T, error) {
|
||||
var zero T
|
||||
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(value, formatPrefix))
|
||||
if err != nil {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
for _, key := range c.keys {
|
||||
a, err := aead.New(key)
|
||||
if err != nil {
|
||||
return zero, errors.Wrap(err, "cookiex: cannot create AEAD")
|
||||
}
|
||||
if len(raw) < a.NonceSize() {
|
||||
continue
|
||||
}
|
||||
plaintext, err := a.Open(nil, raw[:a.NonceSize()], raw[a.NonceSize():], c.aad(name))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var env envelope
|
||||
if err := json.Unmarshal(plaintext, &env); err != nil {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
if c.maxAge > 0 && c.now().Sub(time.Unix(env.IssuedAt, 0)) > c.maxAge {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
var out T
|
||||
if err := json.Unmarshal(env.Values, &out); err != nil {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
|
||||
// Set seals value into cookie.Value and writes the cookie to w. All other
|
||||
// attributes (name, path, domain, max-age, secure, http-only, same-site) must
|
||||
// be set by the caller on the cookie.
|
||||
func (c *Codec[T]) Set(w http.ResponseWriter, cookie *http.Cookie, value T) error {
|
||||
var encoded string
|
||||
var err error
|
||||
if c.legacy.encode {
|
||||
encoded, err = c.sealLegacy(cookie.Name, value)
|
||||
} else {
|
||||
encoded, err = c.seal(cookie.Name, value)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(encoded) > maxCookieValueLength {
|
||||
return errors.Errorf("cookiex: encoded cookie %q exceeds %d bytes", cookie.Name, maxCookieValueLength)
|
||||
}
|
||||
cookie.Value = encoded
|
||||
http.SetCookie(w, cookie)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the value of the first cookie named name that decodes. It
|
||||
// returns http.ErrNoCookie when no cookie with that name is present, and
|
||||
// ErrInvalidCookie when none of the present values decode.
|
||||
func (c *Codec[T]) Get(r *http.Request, name string) (T, error) {
|
||||
return c.GetMatching(r, name, nil)
|
||||
}
|
||||
|
||||
// GetMatching returns the value of the first cookie named name that decodes
|
||||
// and matches. Requests can carry multiple cookies with the same name, for
|
||||
// example one from a parent and one from a sub domain; match selects among
|
||||
// them. A nil match matches any value. It returns http.ErrNoCookie when no
|
||||
// cookie with that name is present, and ErrInvalidCookie when none of the
|
||||
// present values decode and match.
|
||||
func (c *Codec[T]) GetMatching(r *http.Request, name string, match func(T) bool) (T, error) {
|
||||
if match == nil {
|
||||
match = func(T) bool { return true }
|
||||
}
|
||||
var zero T
|
||||
found := false
|
||||
for _, ck := range r.Cookies() {
|
||||
if ck.Name != name {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
// Refuse oversized values before doing any decode work, matching the
|
||||
// limit Set enforces and securecookie's own decode-side check.
|
||||
if len(ck.Value) > maxCookieValueLength {
|
||||
continue
|
||||
}
|
||||
var value T
|
||||
var err error
|
||||
if strings.HasPrefix(ck.Value, formatPrefix) {
|
||||
value, err = c.open(name, ck.Value)
|
||||
} else {
|
||||
value, err = c.openLegacy(name, ck.Value)
|
||||
}
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if match(value) {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return zero, errors.WithStack(http.ErrNoCookie)
|
||||
}
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
|
||||
// Expire overwrites the cookie with an expired empty one, deleting it from
|
||||
// the client.
|
||||
func (c *Codec[T]) Expire(w http.ResponseWriter, cookie *http.Cookie) {
|
||||
cookie.Value = ""
|
||||
cookie.MaxAge = -1
|
||||
cookie.Expires = time.Unix(1, 0)
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
136
oryx/cookiex/legacy_securecookie.go
Normal file
136
oryx/cookiex/legacy_securecookie.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cookiex
|
||||
|
||||
// This file isolates the gorilla/securecookie dependency. It exists so that
|
||||
// cookies sealed by the previous securecookie-based stores keep decoding
|
||||
// during the migration, and (behind WithLegacyEncode) so that freshly minted
|
||||
// cookies stay readable by old pods during the stage-1 rollout. Delete it,
|
||||
// and the securecookie dependency, once ory_legacy_cookie_decodes_total has
|
||||
// flatlined fleet-wide for a full max-cookie-lifespan window.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
|
||||
// legacyDecodes counts cookies that were successfully decoded via the legacy
|
||||
// securecookie fallback. It is registered on the Prometheus default
|
||||
// registerer, which Kratos and Hydra expose on /metrics/prometheus.
|
||||
var legacyDecodes = promauto.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "ory_legacy_cookie_decodes_total",
|
||||
Help: "Number of cookies successfully decoded via the legacy securecookie fallback, by codec purpose.",
|
||||
}, []string{"purpose"})
|
||||
|
||||
// LegacySecureCookieKeyPairs derives the (hashKey, blockKey) pairs the
|
||||
// securecookie-based cookie stores used: each secret signs, and its SHA-256
|
||||
// digest encrypts.
|
||||
func LegacySecureCookieKeyPairs(secrets [][]byte) [][]byte {
|
||||
pairs := make([][]byte, 0, 2*len(secrets))
|
||||
for _, secret := range secrets {
|
||||
block := sha256.Sum256(secret)
|
||||
pairs = append(pairs, secret, block[:])
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
// WithLegacyKeyPairs enables decoding cookies in the legacy securecookie
|
||||
// format, sealed under the given (hashKey, blockKey) pairs. While this option
|
||||
// is in use, T must be unmarshalable from a flat JSON object with string
|
||||
// values; WithLegacyEncode additionally requires T to marshal to one. Remove
|
||||
// once all cookies sealed in the legacy format have expired.
|
||||
func WithLegacyKeyPairs(pairs ...[]byte) Option {
|
||||
return func(c *config) { c.legacyKeyPairs = pairs }
|
||||
}
|
||||
|
||||
type legacyState struct {
|
||||
codecs []securecookie.Codec
|
||||
encode bool
|
||||
}
|
||||
|
||||
func newLegacyState(cfg config, maxAge time.Duration) legacyState {
|
||||
codecs := securecookie.CodecsFromPairs(cfg.legacyKeyPairs...)
|
||||
for _, codec := range codecs {
|
||||
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
||||
sc.MaxAge(int(maxAge / time.Second))
|
||||
}
|
||||
}
|
||||
return legacyState{codecs: codecs, encode: cfg.legacyEncode}
|
||||
}
|
||||
|
||||
// openLegacy decodes a securecookie-sealed value and bridges it into T via
|
||||
// its JSON representation: legacy payloads are flat string-to-string maps, so
|
||||
// string, time.Time, and UUID fields all round-trip through their JSON string
|
||||
// forms.
|
||||
func (c *Codec[T]) openLegacy(name, value string) (T, error) {
|
||||
var zero T
|
||||
if len(c.legacy.codecs) == 0 {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
values := make(map[any]any)
|
||||
if err := securecookie.DecodeMulti(name, value, &values, c.legacy.codecs...); err != nil {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
flat := make(map[string]string, len(values))
|
||||
for k, v := range values {
|
||||
key, keyOK := k.(string)
|
||||
val, valOK := v.(string)
|
||||
if !keyOK || !valOK {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
flat[key] = val
|
||||
}
|
||||
bridge, err := json.Marshal(flat)
|
||||
if err != nil {
|
||||
return zero, errors.Wrap(err, "cookiex: cannot bridge legacy cookie")
|
||||
}
|
||||
var out T
|
||||
if err := json.Unmarshal(bridge, &out); err != nil {
|
||||
return zero, errors.WithStack(ErrInvalidCookie)
|
||||
}
|
||||
legacyDecodes.WithLabelValues(c.purpose).Inc()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WithLegacyEncode makes Set seal in the legacy securecookie format under the
|
||||
// first legacy key pair, so pods that only understand the legacy format can
|
||||
// read freshly minted cookies during a rolling deploy. Requires
|
||||
// WithLegacyKeyPairs. This is stage 1 of the rollout; a follow-up removes the
|
||||
// option, flipping encoding to the v1 format. JSON null values inside the
|
||||
// payload are coerced to empty strings by the bridge; do not use pointer-typed
|
||||
// fields while legacy encode is enabled.
|
||||
func WithLegacyEncode() Option {
|
||||
return func(c *config) { c.legacyEncode = true }
|
||||
}
|
||||
|
||||
// sealLegacy bridges T through its JSON representation into the flat
|
||||
// string-to-string map that the securecookie stores used.
|
||||
func (c *Codec[T]) sealLegacy(name string, value T) (string, error) {
|
||||
buf, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot marshal cookie value")
|
||||
}
|
||||
var flat map[string]string
|
||||
if err := json.Unmarshal(buf, &flat); err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
|
||||
}
|
||||
if flat == nil {
|
||||
return "", errors.New("cookiex: payload must be a flat JSON object with string values while legacy encode is enabled")
|
||||
}
|
||||
values := make(map[any]any, len(flat))
|
||||
for k, v := range flat {
|
||||
values[k] = v
|
||||
}
|
||||
encoded, err := securecookie.EncodeMulti(name, values, c.legacy.codecs[0])
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "cookiex: cannot encode legacy cookie")
|
||||
}
|
||||
return encoded, nil
|
||||
}
|
||||
141
oryx/corsx/check_origin.go
Normal file
141
oryx/corsx/check_origin.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package corsx
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// CheckOrigin is a function that can be used well with cors.Options.AllowOriginRequestFunc.
|
||||
// It checks whether the origin is allowed following the same behavior as github.com/rs/cors.
|
||||
//
|
||||
// When legacyAllowInsecureOrigins is false (the default), wildcard patterns are
|
||||
// only honored when ClassifyOrigin reports them as bounded at a registrable
|
||||
// domain. Pass true to opt into legacy (trusting) behavior for unbounded
|
||||
// wildcards.
|
||||
//
|
||||
// TODO: legacyAllowInsecureOrigins grandfathers a fixed set of projects through
|
||||
// a time-boxed migration window (feature_flags.legacy_allow_insecure_origins).
|
||||
// Once those projects move to bounded wildcards and the entitlement is revoked,
|
||||
// drop this parameter and always enforce the boundary.
|
||||
//
|
||||
// Recommended usage for hot-reloadable origins:
|
||||
//
|
||||
// func (p *Config) cors(ctx context.Context, prefix string) (cors.Options, bool) {
|
||||
// opts, enabled := p.GetProvider(ctx).CORS(prefix, cors.Options{
|
||||
// AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
|
||||
// AllowedHeaders: []string{"Authorization", "Content-Type", "Cookie"},
|
||||
// ExposedHeaders: []string{"Content-Type", "Set-Cookie"},
|
||||
// AllowCredentials: true,
|
||||
// })
|
||||
// opts.AllowOriginRequestFunc = func(r *http.Request, origin string) bool {
|
||||
// // load the origins from the config on every request to allow hot-reloading
|
||||
// allowedOrigins := p.GetProvider(r.Context()).Strings(prefix + ".cors.allowed_origins")
|
||||
// return corsx.CheckOrigin(allowedOrigins, origin, false)
|
||||
// }
|
||||
// return opts, enabled
|
||||
// }
|
||||
func CheckOrigin(allowedOrigins []string, origin string, legacyAllowInsecureOrigins bool) bool {
|
||||
if len(allowedOrigins) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, o := range allowedOrigins {
|
||||
if o == "*" {
|
||||
// allow all origins
|
||||
return true
|
||||
}
|
||||
// Note: for origins and methods matching, the spec requires a case-sensitive matching.
|
||||
// As it may be error-prone, we chose to ignore the spec here.
|
||||
// https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/cors.go#L132-L133
|
||||
o = strings.ToLower(o)
|
||||
prefix, suffix, found := strings.Cut(o, "*")
|
||||
if !found {
|
||||
// not a pattern, check for equality
|
||||
if o == origin {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Only honor wildcards bounded at a registrable domain unless the caller
|
||||
// explicitly opts into legacy insecure matching. See ClassifyOrigin.
|
||||
if !legacyAllowInsecureOrigins && ClassifyOrigin(o).IsUnsafeWildcard() {
|
||||
continue
|
||||
}
|
||||
// inspired by https://github.com/rs/cors/blob/066574eebbd0f5f1b6cd1154a160cc292ac1835e/utils.go#L15
|
||||
if len(origin) >= len(prefix)+len(suffix) && strings.HasPrefix(origin, prefix) && strings.HasSuffix(origin, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// OriginPattern describes a CORS origin or return-URL host pattern: whether it
|
||||
// uses a wildcard and, if so, whether that wildcard is safely bounded at a
|
||||
// registrable domain.
|
||||
type OriginPattern struct {
|
||||
// HasWildcard reports whether the pattern contains a "*".
|
||||
HasWildcard bool
|
||||
|
||||
// BoundedWildcard reports whether the "*" is confined to a subdomain label
|
||||
// and the fixed domain that follows it is a registrable domain (an eTLD+1,
|
||||
// e.g. "example.com" or "example.co.uk"). Every host the pattern can match
|
||||
// then shares that one customer-owned registrable domain, so an attacker
|
||||
// cannot register a matching host. Always false when HasWildcard is false.
|
||||
BoundedWildcard bool
|
||||
|
||||
// Base is the fixed domain that follows the wildcard label — "example.com"
|
||||
// for "*.example.com", "com" for "*.com". It is empty for non-wildcards and
|
||||
// for bare or trailing wildcards where no domain follows the "*". When
|
||||
// BoundedWildcard is false, Base names the offending suffix, which is the
|
||||
// actionable signal for reporting why a wildcard was rejected.
|
||||
Base string
|
||||
}
|
||||
|
||||
// IsUnsafeWildcard reports whether the pattern is a wildcard that is NOT bounded
|
||||
// at a registrable domain. Such a wildcard would match an attacker-registrable
|
||||
// host (e.g. "https://*foo.com" matches "https://evilfoo.com"), so it must be
|
||||
// rejected unless the caller explicitly opts into legacy insecure matching. This
|
||||
// is the dominant question at call sites that gate, drop, or reject wildcard
|
||||
// origins and return URLs.
|
||||
func (p OriginPattern) IsUnsafeWildcard() bool {
|
||||
return p.HasWildcard && !p.BoundedWildcard
|
||||
}
|
||||
|
||||
// ClassifyOrigin inspects a CORS origin or bare host pattern and reports whether
|
||||
// it is a wildcard and, if so, whether the wildcard is safely bounded at a
|
||||
// registrable domain. Only the text from the last "*" onward is inspected, so
|
||||
// the result is identical whether pattern carries a scheme or is a bare host; a
|
||||
// trailing ":port" is ignored. Examples:
|
||||
//
|
||||
// - "https://*.example.com" → {HasWildcard: true, BoundedWildcard: true, Base: "example.com"}
|
||||
// - "https://*foo.com" → {HasWildcard: true, Base: "com"} (dot-less; base is a public suffix)
|
||||
// - "https://*.com" → {HasWildcard: true, Base: "com"} (public suffix, not registrable)
|
||||
// - "https://www.ory.*" → {HasWildcard: true} (trailing; no domain follows)
|
||||
// - "https://exact.foo.com" → {} (no wildcard)
|
||||
func ClassifyOrigin(pattern string) OriginPattern {
|
||||
i := strings.LastIndexByte(pattern, '*')
|
||||
if i < 0 {
|
||||
return OriginPattern{}
|
||||
}
|
||||
p := OriginPattern{HasWildcard: true}
|
||||
// The fixed domain is everything after the first "." that follows the last
|
||||
// "*" — i.e. the label containing the wildcard is dropped. Without such a dot
|
||||
// the wildcard is trailing or bare, so no registrable domain follows.
|
||||
_, base, found := strings.Cut(pattern[i:], ".")
|
||||
if !found {
|
||||
return p
|
||||
}
|
||||
base = strings.ToLower(base)
|
||||
if host, _, found := strings.Cut(base, ":"); found {
|
||||
base = host // Drop a trailing ":port".
|
||||
}
|
||||
p.Base = base
|
||||
// EffectiveTLDPlusOne returns an error when base is itself a public suffix
|
||||
// (e.g. "com", "co.uk", "vercel.app") or otherwise has no registrable domain.
|
||||
_, err := publicsuffix.EffectiveTLDPlusOne(base)
|
||||
p.BoundedWildcard = err == nil
|
||||
return p
|
||||
}
|
||||
46
oryx/corsx/cmd.go
Normal file
46
oryx/corsx/cmd.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package corsx
|
||||
|
||||
// HelpMessage returns a string containing information on setting up this CORS middleware.
|
||||
func HelpMessage() string {
|
||||
return `- CORS_ENABLED: Switch CORS support on (true) or off (false). Default is off (false).
|
||||
|
||||
Example: CORS_ENABLED=true
|
||||
|
||||
- CORS_ALLOWED_ORIGINS: A list of origins (comma separated values) a cross-domain request can be executed from.
|
||||
If the special * value is present in the list, all origins will be allowed. An origin may contain a wildcard (*)
|
||||
to replace 0 or more characters (i.e.: http://*.domain.com). Usage of wildcards implies a small performance penality.
|
||||
Only one wildcard can be used per origin. The default value is *.
|
||||
|
||||
Example: CORS_ALLOWED_ORIGINS=http://*.domain.com,http://*.domain2.com
|
||||
|
||||
- CORS_ALLOWED_METHODS: A list of methods (comma separated values) the client is allowed to use with cross-domain
|
||||
requests. Default value is simple methods (GET and POST).
|
||||
|
||||
Example: CORS_ALLOWED_METHODS=POST,GET,PUT
|
||||
|
||||
- CORS_ALLOWED_CREDENTIALS: Indicates whether the request can include user credentials like cookies, HTTP authentication
|
||||
or client side SSL certificates.
|
||||
|
||||
Default: CORS_ALLOWED_CREDENTIALS=false
|
||||
Example: CORS_ALLOWED_CREDENTIALS=true
|
||||
|
||||
- CORS_DEBUG: Debugging flag adds additional output to debug server side CORS issues.
|
||||
|
||||
Default: CORS_DEBUG=false
|
||||
Example: CORS_DEBUG=true
|
||||
|
||||
- CORS_MAX_AGE: Indicates how long (in seconds) the results of a preflight request can be cached. The default is 0
|
||||
which stands for no max age.
|
||||
|
||||
Default: CORS_MAX_AGE=0
|
||||
Example: CORS_MAX_AGE=10
|
||||
|
||||
- CORS_ALLOWED_HEADERS: A list of non simple headers (comma separated values) the client is allowed to use with
|
||||
cross-domain requests.
|
||||
|
||||
- CORS_EXPOSED_HEADERS: Indicates which headers (comma separated values) are safe to expose to the API of a
|
||||
CORS API specification.`
|
||||
}
|
||||
33
oryx/corsx/defaults.go
Normal file
33
oryx/corsx/defaults.go
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package corsx
|
||||
|
||||
// CORSRequestHeadersSafelist We add the safe list cors accept headers
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_request_header
|
||||
var CORSRequestHeadersSafelist = []string{"Accept", "Content-Type", "Content-Length", "Accept-Language", "Content-Language"}
|
||||
|
||||
// CORSResponseHeadersSafelist We add the safe list cors expose headers
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header
|
||||
var CORSResponseHeadersSafelist = []string{"Set-Cookie", "Cache-Control", "Expires", "Last-Modified", "Pragma", "Content-Length", "Content-Language", "Content-Type"}
|
||||
|
||||
// CORSDefaultAllowedMethods Default allowed methods
|
||||
var CORSDefaultAllowedMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
|
||||
|
||||
// CORSRequestHeadersExtended Extended list of request headers
|
||||
// these will be concatenated with the safelist
|
||||
var CORSRequestHeadersExtended = []string{"Authorization", "X-CSRF-TOKEN"}
|
||||
|
||||
// CORSResponseHeadersExtended Extended list of response headers
|
||||
// these will be concatenated with the safelist
|
||||
var CORSResponseHeadersExtended = []string{}
|
||||
|
||||
// CORSDefaultMaxAge max age for cache of preflight request result
|
||||
// default is 5 seconds
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
|
||||
var CORSDefaultMaxAge = 5
|
||||
|
||||
// CORSAllowCredentials default value for allow credentials
|
||||
// this is required for cookies to be sent by the browser
|
||||
// we always want this since we are using cookies for authentication most of the time
|
||||
var CORSAllowCredentials = true
|
||||
34
oryx/corsx/middleware.go
Normal file
34
oryx/corsx/middleware.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package corsx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/cors"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
// ContextualizedMiddleware is a context-aware CORS middleware. It allows hot-reloading CORS configuration using
|
||||
// the HTTP request context.
|
||||
//
|
||||
// n := negroni.New()
|
||||
// n.UseFunc(ContextualizedMiddleware(func(context.Context) (opts cors.Options, enabled bool) {
|
||||
// panic("implement me")
|
||||
// })
|
||||
// // ...
|
||||
//
|
||||
// Deprecated: because this is not really practical to use, you should use CheckOrigin as the cors.Options.AllowOriginRequestFunc instead.
|
||||
func ContextualizedMiddleware(provider func(context.Context) (opts cors.Options, enabled bool)) negroni.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
options, enabled := provider(r.Context())
|
||||
if !enabled {
|
||||
next(rw, r)
|
||||
return
|
||||
}
|
||||
|
||||
cors.New(options).Handler(next).ServeHTTP(rw, r)
|
||||
}
|
||||
}
|
||||
28
oryx/corsx/normalize.go
Normal file
28
oryx/corsx/normalize.go
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package corsx
|
||||
|
||||
import "net/url"
|
||||
|
||||
// NormalizeOrigins normalizes the CORS origins.
|
||||
func NormalizeOrigins(origins []url.URL) []string {
|
||||
results := make([]string, len(origins))
|
||||
for k, o := range origins {
|
||||
results[k] = o.Scheme + "://" + o.Host
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// NormalizeOriginStrings normalizes the CORS origins from string representation
|
||||
func NormalizeOriginStrings(origins []string) ([]string, error) {
|
||||
results := make([]string, len(origins))
|
||||
for k, o := range origins {
|
||||
u, err := url.ParseRequestURI(o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results[k] = u.Scheme + "://" + u.Host
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
32
oryx/crdbx/isolation.go
Normal file
32
oryx/crdbx/isolation.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package crdbx
|
||||
|
||||
import (
|
||||
"github.com/ory/pop/v6"
|
||||
"github.com/ory/x/dbal"
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
// SetTransactionReadCommitted lowers the isolation of the current transaction
|
||||
// to READ COMMITTED on CockroachDB. Multi-statement read transactions at
|
||||
// SERIALIZABLE must refresh their whole read set when a concurrent write
|
||||
// bumps the transaction timestamp, and surface RETRY_SERIALIZABLE to the
|
||||
// client when the refresh fails; under READ COMMITTED every statement reads
|
||||
// at its own timestamp and those conflicts are absorbed server-side. Must be
|
||||
// called before the transaction runs its first query. Postgres already runs
|
||||
// statements at READ COMMITTED by default, and MySQL's SET TRANSACTION
|
||||
// applies to the next transaction rather than the current one, so every
|
||||
// other dialect is a no-op. YugabyteDB maps READ COMMITTED to snapshot
|
||||
// isolation unless yb_enable_read_committed_isolation is enabled, so it stays
|
||||
// on the normal transaction retry path instead of using this CockroachDB-only
|
||||
// override.
|
||||
func SetTransactionReadCommitted(c *pop.Connection) error {
|
||||
if c.Dialect.Name() != dbal.DriverCockroachDB {
|
||||
// Only CockroachDB supports and needs this.
|
||||
return nil
|
||||
}
|
||||
|
||||
return sqlcon.HandleError(c.RawQuery("SET TRANSACTION ISOLATION LEVEL READ COMMITTED").Exec())
|
||||
}
|
||||
24
oryx/crdbx/readonly.go
Normal file
24
oryx/crdbx/readonly.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package crdbx
|
||||
|
||||
import (
|
||||
"github.com/ory/pop/v6"
|
||||
|
||||
"github.com/ory/x/dbal"
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
// SetTransactionReadOnly sets the current transaction to read only for
|
||||
// CockroachDB, PostgreSQL, and YugabyteDB, which all let the server skip
|
||||
// write-path bookkeeping and reject accidental writes. Must be called before the
|
||||
// transaction runs its first query. MySQL's SET TRANSACTION applies to the
|
||||
// next transaction rather than the current one, and SQLite has no
|
||||
// equivalent, so every other dialect is a no-op.
|
||||
func SetTransactionReadOnly(c *pop.Connection) error {
|
||||
if dbal.IsPostgresCompatible(c.Dialect.Name()) {
|
||||
return sqlcon.HandleError(c.RawQuery("SET TRANSACTION READ ONLY").Exec())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
110
oryx/crdbx/staleness.go
Normal file
110
oryx/crdbx/staleness.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package crdbx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/ory/x/dbal"
|
||||
|
||||
"github.com/ory/pop/v6"
|
||||
|
||||
"github.com/ory/x/sqlcon"
|
||||
)
|
||||
|
||||
// Control API consistency guarantees
|
||||
//
|
||||
// swagger:model consistencyRequestParameters
|
||||
type ConsistencyRequestParameters struct {
|
||||
// Read Consistency Level (preview)
|
||||
//
|
||||
// The read consistency level determines the consistency guarantee for reads:
|
||||
//
|
||||
// - strong (slow): The read is guaranteed to return the most recent data committed at the start of the read.
|
||||
// - eventual (very fast): The result will return data that is about 4.8 seconds old.
|
||||
//
|
||||
// The default consistency guarantee can be changed in the Ory Network Console or using the Ory CLI with
|
||||
// `ory patch project --replace '/previews/default_read_consistency_level="strong"'`.
|
||||
//
|
||||
// Setting the default consistency level to `eventual` may cause regressions in the future as we add consistency
|
||||
// controls to more APIs. Currently, the following APIs will be affected by this setting:
|
||||
//
|
||||
// - `GET /admin/identities`
|
||||
//
|
||||
// This feature is in preview and only available in Ory Network.
|
||||
//
|
||||
// required: false
|
||||
// in: query
|
||||
Consistency ConsistencyLevel `json:"consistency"`
|
||||
}
|
||||
|
||||
// ConsistencyLevel is the consistency level.
|
||||
// swagger:enum ConsistencyLevel
|
||||
type ConsistencyLevel string
|
||||
|
||||
const (
|
||||
// ConsistencyLevelUnset is the unset / default consistency level.
|
||||
ConsistencyLevelUnset ConsistencyLevel = ""
|
||||
// ConsistencyLevelStrong is the strong consistency level.
|
||||
ConsistencyLevelStrong ConsistencyLevel = "strong"
|
||||
// ConsistencyLevelEventual is the eventual consistency level using follower read timestamps.
|
||||
ConsistencyLevelEventual ConsistencyLevel = "eventual"
|
||||
)
|
||||
|
||||
// ConsistencyLevelFromRequest extracts the consistency level from a request.
|
||||
func ConsistencyLevelFromRequest(r *http.Request) ConsistencyLevel {
|
||||
return ConsistencyLevelFromString(r.URL.Query().Get("consistency"))
|
||||
}
|
||||
|
||||
// ConsistencyLevelFromString converts a string to a ConsistencyLevel.
|
||||
// If the string is not recognized or unset, ConsistencyLevelStrong is returned.
|
||||
func ConsistencyLevelFromString(in string) ConsistencyLevel {
|
||||
switch in {
|
||||
case string(ConsistencyLevelStrong):
|
||||
return ConsistencyLevelStrong
|
||||
case string(ConsistencyLevelEventual):
|
||||
return ConsistencyLevelEventual
|
||||
case string(ConsistencyLevelUnset):
|
||||
return ConsistencyLevelUnset
|
||||
}
|
||||
return ConsistencyLevelStrong
|
||||
}
|
||||
|
||||
// SetTransactionConsistency sets the transaction consistency level for CockroachDB.
|
||||
func SetTransactionConsistency(c *pop.Connection, level ConsistencyLevel, fallback ConsistencyLevel) error {
|
||||
q := getTransactionConsistencyQuery(c.Dialect.Name(), level, fallback)
|
||||
if len(q) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sqlcon.HandleError(c.RawQuery(q).Exec())
|
||||
}
|
||||
|
||||
const transactionFollowerReadTimestamp = "SET TRANSACTION AS OF SYSTEM TIME follower_read_timestamp()"
|
||||
|
||||
func getTransactionConsistencyQuery(dialect string, level ConsistencyLevel, fallback ConsistencyLevel) string {
|
||||
if dialect != dbal.DriverCockroachDB {
|
||||
// Only CockroachDB supports this.
|
||||
return ""
|
||||
}
|
||||
|
||||
switch level {
|
||||
case ConsistencyLevelStrong:
|
||||
// Nothing to do
|
||||
return ""
|
||||
case ConsistencyLevelEventual:
|
||||
// Jumps to end of function
|
||||
case ConsistencyLevelUnset:
|
||||
fallthrough
|
||||
default:
|
||||
if fallback != ConsistencyLevelEventual {
|
||||
// Nothing to do
|
||||
return ""
|
||||
}
|
||||
|
||||
// Jumps to end of function
|
||||
}
|
||||
|
||||
return transactionFollowerReadTimestamp
|
||||
}
|
||||
26
oryx/dbal/canonicalize.go
Normal file
26
oryx/dbal/canonicalize.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package dbal
|
||||
|
||||
const (
|
||||
DriverMySQL = "mysql"
|
||||
DriverCockroachDB = "cockroach"
|
||||
DriverPostgreSQL = "postgres"
|
||||
// DriverYugabyteDB is the dialect name YugabyteDB connections report. It is
|
||||
// a commercial (Ory Enterprise License) dialect that speaks the PostgreSQL
|
||||
// wire protocol and SQL dialect, so most SQL paths treat it like postgres.
|
||||
DriverYugabyteDB = "yugabyte"
|
||||
)
|
||||
|
||||
// IsPostgresCompatible reports whether dialect speaks the PostgreSQL SQL
|
||||
// dialect. It does not imply support for CockroachDB-specific syntax or
|
||||
// features.
|
||||
func IsPostgresCompatible(dialect string) bool {
|
||||
switch dialect {
|
||||
case DriverPostgreSQL, DriverCockroachDB, DriverYugabyteDB:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
14
oryx/dbal/driver.go
Normal file
14
oryx/dbal/driver.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package dbal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsSQLite returns true if the connection is a SQLite string.
|
||||
func IsSQLite(dsn string) bool {
|
||||
scheme := strings.Split(dsn, "://")[0]
|
||||
return scheme == "sqlite" || scheme == "sqlite3"
|
||||
}
|
||||
47
oryx/dbal/dsn.go
Normal file
47
oryx/dbal/dsn.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package dbal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var sqliteMemoryRegexp = regexp.MustCompile(`^sqlite://file:.+\?.*&?mode=memory($|&.*)|sqlite://(file:)?:memory:\?.*$|^(:memory:|memory)$`)
|
||||
|
||||
// IsMemorySQLite returns true if a given DSN string is pointing to a SQLite database.
|
||||
//
|
||||
// SQLite can be written in different styles depending on the use case
|
||||
// - just in memory
|
||||
// - shared connection
|
||||
// - shared but unique in the same process
|
||||
// see: https://sqlite.org/inmemorydb.html
|
||||
func IsMemorySQLite(dsn string) bool { return sqliteMemoryRegexp.MatchString(dsn) }
|
||||
|
||||
// NewSQLiteTestDatabase creates a new, unique SQLite database
|
||||
// which is shared amongst all callers and identified by an individual file name.
|
||||
// The database file is created in the system's temporary directory, and not actively
|
||||
// removed to allow debugging in case of test failures.
|
||||
func NewSQLiteTestDatabase(t testing.TB) string {
|
||||
fn, err := os.MkdirTemp("", "sqlite-test-db-*")
|
||||
require.NoError(t, err)
|
||||
return NewSQLiteDatabase(fn)
|
||||
}
|
||||
|
||||
// NewSQLiteDatabase creates a new on-disk, unique SQLite database
|
||||
// which is shared amongst all callers and identified by an individual file name.
|
||||
// This is sometimes necessary over a in-memory database, for example when multiple tests/goroutines run in parallel
|
||||
// and access the same table.
|
||||
// This would result in a locking error from SQLite when running in-memory.
|
||||
// Additionally, shared cache mode is deprecated and discouraged, and the problem is better solved with the WAL,
|
||||
// according to official docs.
|
||||
//
|
||||
// In tests, use [NewSQLiteTestDatabase] instead.
|
||||
func NewSQLiteDatabase(name string) string {
|
||||
return fmt.Sprintf("sqlite://%s/db.sqlite?_fk=true&_busy_timeout=100000&_pragma=journal_mode(WAL)&_time_format=sqlite", name)
|
||||
}
|
||||
62
oryx/dbal/testhelpers.go
Normal file
62
oryx/dbal/testhelpers.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package dbal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/pop/v6"
|
||||
"github.com/ory/x/fsx"
|
||||
"github.com/ory/x/sqlcon/dockertest"
|
||||
)
|
||||
|
||||
var hashDumpRegex = regexp.MustCompile(`-- migrations hash: ([^\n]+)\n`)
|
||||
|
||||
func RestoreFromSchemaDump(t testing.TB, c *pop.Connection, migrations fs.FS, writeTo string) func(testing.TB) {
|
||||
newHash, err := fsx.DirHash(migrations)
|
||||
require.NoError(t, err)
|
||||
|
||||
dumpFilename := filepath.Join(writeTo, c.Dialect.Name()+"_dump.sql")
|
||||
|
||||
updateDump := func(t testing.TB) {
|
||||
dump := dockertest.DumpSchema(t, c)
|
||||
f, err := os.OpenFile(dumpFilename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
_, _ = fmt.Fprintf(f, "-- migrations hash: %x\n\n%s", newHash, dump)
|
||||
t.Fatal("database schema restored from migrations and dump updated, please re-run the test")
|
||||
}
|
||||
|
||||
dump, err := os.ReadFile(dumpFilename)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return updateDump
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
matches := hashDumpRegex.FindSubmatch(dump)
|
||||
if len(matches) != 2 {
|
||||
return updateDump
|
||||
}
|
||||
|
||||
currentHash, err := hex.DecodeString(string(matches[1]))
|
||||
require.NoError(t, err)
|
||||
|
||||
if !bytes.Equal(newHash, currentHash) {
|
||||
return updateDump
|
||||
}
|
||||
|
||||
require.NoError(t, c.RawQuery(string(dump)).Exec())
|
||||
return nil
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user