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

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

8
oryx/.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
.bin
vendor
.idea
coverage.txt
node_modules/
**/*.pprof
**/memstats.*.txt
.vscode/settings.json

1
oryx/.goimportsignore Normal file
View File

@@ -0,0 +1 @@
vendor/

9
oryx/.golangci.yml Normal file
View File

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

0
oryx/.nancy-ignore Normal file
View File

5
oryx/.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
.github/pull_request_template.md
clidoc/testdata/
healthx/openapi/patch.yaml
.snapshots
fixtures

3
oryx/.reference-ignore Normal file
View File

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

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

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

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

View 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
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

61
oryx/Makefile Normal file
View 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
View 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")
}

View 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
View 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...)
}

View 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
View 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
View 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
View 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
View 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```"
})
}

View 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

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

View File

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

22
oryx/configx/context.go Normal file
View 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
View 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")
}

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

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

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

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

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

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

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

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

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

View 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
View 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"
)

View 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

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

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

View 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

View 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

File diff suppressed because it is too large Load Diff

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

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

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

View 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

File diff suppressed because it is too large Load Diff

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

View 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

View 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

View 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

File diff suppressed because it is too large Load Diff

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

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

View File

@@ -0,0 +1,11 @@
{
"providers": [
{
"id": "google",
"client_id": "client@example.com"
},
{
"client_id": "some@example.com"
}
]
}

View File

@@ -0,0 +1,2 @@
providers:
- id: google

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

View 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
}
]
}
}
}

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

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

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

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