diff --git a/.circleci/config.yml b/.circleci/config.yml index c8bbf8141fb..1920d8183ca 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,75 +5,103 @@ version: 2 jobs: lint: docker: - - image: circleci/golang:1.13 + - + image: circleci/golang:1.13 environment: - GO111MODULE=on working_directory: /go/src/github.com/ory/kratos steps: - checkout - - restore_cache: + - + restore_cache: keys: - go-mod-v1-{{ checksum "go.sum" }} - - run: go mod download - - save_cache: + - + run: go mod download + - + save_cache: key: go-mod-v1-{{ checksum "go.sum" }} paths: - "/go/pkg/mod" - - run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.20.1 - - run: make lint + - + run: curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.20.1 + - + run: make lint test: docker: - - image: circleci/golang:1.13 + - + image: circleci/golang:1.13 environment: - GO111MODULE=on - TEST_SELFSERVICE_OIDC_HYDRA_ADMIN=http://127.0.0.1:4445 - TEST_SELFSERVICE_OIDC_HYDRA_PUBLIC=http://127.0.0.1:4444 - TEST_SELFSERVICE_OIDC_HYDRA_INTEGRATION_ADDR=127.0.0.1:4499 - - TEST_DATABASE_POSTGRESQL=postgres://test:test@localhost:5432/hydra?sslmode=disable - - image: oryd/hydra:v1.0.0 + - TEST_DATABASE_POSTGRESQL=postgres://test:test@localhost:5432/postgres?sslmode=disable + - TEST_DATABASE_MYSQL=mysql://root:test@(localhost:3306)/mysql?parseTime=true +# - TEST_DATABASE_COCKROACHDB=cockroach://root@localhost:26257/defaultdb?sslmode=disable + - + image: postgres:9.6 + environment: + - POSTGRES_USER=test + - POSTGRES_PASSWORD=test + - POSTGRES_DB=postgres + - + image: mysql:5.7 + environment: + - MYSQL_ROOT_PASSWORD=test +# - +# image: cockroachdb/cockroach:v19.2.0 +# command: start --insecure + - + image: oryd/hydra:v1.0.0 environment: - DSN=memory - URLS_SELF_ISSUER=http://127.0.0.1:4444/ - URLS_LOGIN=http://127.0.0.1:4499/login - URLS_CONSENT=http://127.0.0.1:4499/consent command: serve all --dangerous-force-http - - image: postgres:9.6 - environment: - - POSTGRES_USER=test - - POSTGRES_PASSWORD=test - - POSTGRES_DB=hydra working_directory: /go/src/github.com/ory/kratos steps: - checkout - setup_remote_docker - - run: + - + run: command: | ./.circleci/release_name.bash echo 'export DOCKER_SHORT_TAG=$CIRCLE_SHA1' >> $BASH_ENV source $BASH_ENV - - run: GO111MODULE=off go get github.com/mattn/goveralls github.com/ory/go-acc - - restore_cache: + - + run: GO111MODULE=off go get github.com/mattn/goveralls github.com/ory/go-acc + - + restore_cache: keys: - go-v1-{{ checksum "go.sum" }} - - run: go mod download - - save_cache: + - + run: go mod download + - + save_cache: key: go-v1-{{ checksum "go.sum" }} paths: - "/go/pkg/mod" - - run: timeout 15 sh -c 'until nc -z $0 $1; do sleep 1; done' 127.0.0.1 4444 - - run: go-acc -o coverage.txt ./... -- -v -failfast -timeout=20m - - run: test -z "$CIRCLE_PR_NUMBER" && goveralls -service=circle-ci -coverprofile=coverage.txt -repotoken=$COVERALLS_REPO_TOKEN || echo "forks are not allowed to push to coveralls" + - + run: timeout 15 sh -c 'until nc -z $0 $1; do sleep 1; done' 127.0.0.1 4444 + - + run: go-acc -o coverage.txt ./... -- -v -failfast -timeout=20m -tags sqlite + - + run: test -z "$CIRCLE_PR_NUMBER" && goveralls -service=circle-ci -coverprofile=coverage.txt -repotoken=$COVERALLS_REPO_TOKEN || echo "forks are not allowed to push to coveralls" workflows: version: 2 "test": jobs: - - lint: + - + lint: filters: tags: only: /.*/ - - test: + - + test: filters: tags: only: /.*/ \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 1e0e17736b4..610e025e88b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ tmp scripts .idea .git/ +database.yaml diff --git a/.golangci.yml b/.golangci.yml index e34a91c04fc..01c76d54ece 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,18 @@ linters: + enabled: + - deadcode + - errcheck + - gosimple + - govet + - staticcheck + - structcheck + - typecheck + - unused + - bodyclose + - dupl + - gosec + - varcheck + - godox disable: - ineffassign diff --git a/Dockerfile b/Dockerfile index e55fca408dd..b3ab9a4c014 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,6 @@ FROM alpine:3.10 RUN apk add -U --no-cache ca-certificates - -FROM scratch - -COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY kratos /usr/bin/kratos USER 1000 diff --git a/Makefile b/Makefile index 065052c27bf..8870c993dba 100644 --- a/Makefile +++ b/Makefile @@ -65,4 +65,4 @@ sqlbin: resetdb: docker kill hydra_test_database_postgres || true docker rm -f hydra_test_database_postgres || true - docker run --rm --name hydra_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=hydra -d postgres:9.6 + docker run --rm --name hydra_test_database_postgres -p 3445:5432 -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=postgres -d postgres:9.6 diff --git a/README.md b/README.md index 63d65ac86b8..abb8293bb64 100644 --- a/README.md +++ b/README.md @@ -67,4 +67,3 @@ changes in [UPGRADE.md](./UPGRADE.md) and [CHANGELOG.md](./CHANGELOG.md). ### Command line documentation Run `kratos -h` or `kratos help`. - diff --git a/cmd/client/migrate.go b/cmd/client/migrate.go index ddfdcda985c..31d297be969 100644 --- a/cmd/client/migrate.go +++ b/cmd/client/migrate.go @@ -1,15 +1,12 @@ package client import ( - "bufio" + "context" "fmt" "os" - "strings" "github.com/spf13/cobra" - "github.com/ory/x/sqlcon" - "github.com/ory/viper" "github.com/ory/x/cmdx" "github.com/ory/x/flagx" @@ -29,7 +26,7 @@ func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) { var d driver.Driver if flagx.MustGetBool(cmd, "read-from-env") { - d = driver.NewDefaultDriver(logrusx.New(), "", "", "") + d = driver.MustNewDefaultDriver(logrusx.New(), "", "", "") if len(d.Configuration().DSN()) == 0 { fmt.Println(cmd.UsageString()) fmt.Println("") @@ -44,54 +41,57 @@ func (h *MigrateHandler) MigrateSQL(cmd *cobra.Command, args []string) { return } viper.Set(configuration.ViperKeyDSN, args[0]) - d = driver.NewDefaultDriver(logrusx.New(), "", "", "") - } - - reg, ok := d.Registry().(*driver.RegistrySQL) - if !ok { - fmt.Println(cmd.UsageString()) - fmt.Println("") - fmt.Printf("Migrations can only be executed against a SQL-compatible driver but DSN is not a SQL source.\n") - os.Exit(1) - return + d = driver.MustNewDefaultDriver(logrusx.New(), "", "", "") } - scheme := sqlcon.GetDriverName(d.Configuration().DSN()) - plan, err := reg.SchemaMigrationPlan(scheme) - cmdx.Must(err, "An error occurred planning migrations: %s", err) - - fmt.Println("The following migration is planned:") - fmt.Println("") - plan.Render() - - if !flagx.MustGetBool(cmd, "yes") { - fmt.Println("") - fmt.Println("To skip the next question use flag --yes (at your own risk).") - if !askForConfirmation("Do you wish to execute this migration plan?") { - fmt.Println("Migration aborted.") - return - } - } - - n, err := reg.CreateSchemas(scheme) + err := d.Registry().Persister().MigrateUp(context.Background()) cmdx.Must(err, "An error occurred while connecting to SQL: %s", err) - fmt.Printf("Successfully applied %d SQL migrations!\n", n) + fmt.Println("Successfully applied SQL migrations!") + + // if !ok { + // fmt.Println(cmd.UsageString()) + // fmt.Println("") + // fmt.Printf("Migrations can only be executed against a SQL-compatible driver but DSN is not a SQL source.\n") + // os.Exit(1) + // return + // } + // + // scheme := sqlcon.GetDriverName(d.Configuration().DSN()) + // plan, err := reg.SchemaMigrationPlan(scheme) + // cmdx.Must(err, "An error occurred planning migrations: %s", err) + // + // fmt.Println("The following migration is planned:") + // fmt.Println("") + // plan.Render() + // + // if !flagx.MustGetBool(cmd, "yes") { + // fmt.Println("") + // fmt.Println("To skip the next question use flag --yes (at your own risk).") + // if !askForConfirmation("Do you wish to execute this migration plan?") { + // fmt.Println("Migration aborted.") + // return + // } + // } + // + // n, err := reg.CreateSchemas(scheme) + // cmdx.Must(err, "An error occurred while connecting to SQL: %s", err) + // fmt.Printf("Successfully applied %d SQL migrations!\n", n) } -func askForConfirmation(s string) bool { - reader := bufio.NewReader(os.Stdin) - - for { - fmt.Printf("%s [y/n]: ", s) - - response, err := reader.ReadString('\n') - cmdx.Must(err, "%s", err) - - response = strings.ToLower(strings.TrimSpace(response)) - if response == "y" || response == "yes" { - return true - } else if response == "n" || response == "no" { - return false - } - } -} +// func askForConfirmation(s string) bool { +// reader := bufio.NewReader(os.Stdin) +// +// for { +// fmt.Printf("%s [y/n]: ", s) +// +// response, err := reader.ReadString('\n') +// cmdx.Must(err, "%s", err) +// +// response = strings.ToLower(strings.TrimSpace(response)) +// if response == "y" || response == "yes" { +// return true +// } else if response == "n" || response == "no" { +// return false +// } +// } +// } diff --git a/cmd/serve.go b/cmd/serve.go index f24049839fb..26bafbde37e 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -28,7 +28,7 @@ import ( var serveCmd = &cobra.Command{ Use: "serve", Run: func(cmd *cobra.Command, args []string) { - daemon.ServeAll(driver.NewDefaultDriver(logger, BuildVersion, BuildTime, BuildGitHash))(cmd, args) + daemon.ServeAll(driver.MustNewDefaultDriver(logger, BuildVersion, BuildTime, BuildGitHash))(cmd, args) }, } diff --git a/contrib/sql/.soda.yml b/contrib/sql/.soda.yml new file mode 100644 index 00000000000..f265223e605 --- /dev/null +++ b/contrib/sql/.soda.yml @@ -0,0 +1,14 @@ +development: + url: sqlite://a/b + +sqlite: + url: sqlite://a/b + +postgres: + url: postgres://a/b + +mysql: + url: mysql://tcp(a)/b?parseTime=true&multiStatements=true + +cockroach: + url: crdb://a/b diff --git a/contrib/sql/README.md b/contrib/sql/README.md new file mode 100644 index 00000000000..a6d9fa6684e --- /dev/null +++ b/contrib/sql/README.md @@ -0,0 +1,25 @@ +# SQL Migrations + +To create a new [fizz](https://gobuffalo.io/en/docs/db/fizz/) migration run in the project root: + +``` +$ soda generate fizz -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations [name] +``` + +To create SQL migrations, target each database individually and run + +``` +$ soda generate sql -e mysql -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations [name] +$ soda generate sql -e sqlite -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations [name] +$ soda generate sql -e postgres -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations [name] +$ soda generate sql -e cockroach -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations [name] +``` + +or, alternative run + +``` +$ soda generate sql -c ./contrib/sql/.soda.yml -p ./contrib/sql/migrations +``` + +and remove the `sqlite` part from the newly generated file to create a SQL migrations that works with all +aforementioned databases. diff --git a/contrib/sql/migrations/20191120000000_identities.down.fizz b/contrib/sql/migrations/20191120000000_identities.down.fizz new file mode 100644 index 00000000000..8a87d19060f --- /dev/null +++ b/contrib/sql/migrations/20191120000000_identities.down.fizz @@ -0,0 +1 @@ +drop_table("identities") diff --git a/contrib/sql/migrations/20191120000000_identities.up.fizz b/contrib/sql/migrations/20191120000000_identities.up.fizz new file mode 100644 index 00000000000..3d06612a898 --- /dev/null +++ b/contrib/sql/migrations/20191120000000_identities.up.fizz @@ -0,0 +1,41 @@ +create_table("identities") { + t.Column("id", "uuid", {primary: true}) + t.Column("traits_schema_url", "string", {"size": 2048}) + t.Column("traits", "json") + + t.Timestamps() +} + +create_table("identity_credential_types") { + t.Column("id", "uuid", {primary: true}) + t.Column("name", "string", { "size": 32 }) + + t.DisableTimestamps() +} + +add_index("identity_credential_types", "name", {"unique": true}) + +create_table("identity_credentials") { + t.Column("id", "uuid", {primary: true}) + t.Column("config", "json") + + t.Column("identity_credential_type_id", "uuid", { "size": 32 }) + t.Column("identity_id", "uuid") + + t.Timestamps() + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) + t.ForeignKey("identity_credential_type_id", {"identity_credential_types": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("identity_credential_identifiers") { + t.Column("id", "uuid", {primary: true}) + t.Column("identifier", "string", {"size": 255}) + t.Column("identity_credential_id", "uuid") + + t.Timestamps() + + t.ForeignKey("identity_credential_id", {"identity_credentials": ["id"]}, {"on_delete": "cascade"}) +} + +add_index("identity_credential_identifiers", "identifier", {"unique": true}) diff --git a/contrib/sql/migrations/20191121000000_requests.down.fizz b/contrib/sql/migrations/20191121000000_requests.down.fizz new file mode 100644 index 00000000000..6d1b92dfb33 --- /dev/null +++ b/contrib/sql/migrations/20191121000000_requests.down.fizz @@ -0,0 +1,5 @@ +drop_table("selfservice_login_requests") +drop_table("selfservice_login_request_methods") +drop_table("selfservice_registration_requests") +drop_table("selfservice_registration_methods") +drop_table("selfservice_profile_management_requests") \ No newline at end of file diff --git a/contrib/sql/migrations/20191121000000_requests.up.fizz b/contrib/sql/migrations/20191121000000_requests.up.fizz new file mode 100644 index 00000000000..05f5105bb46 --- /dev/null +++ b/contrib/sql/migrations/20191121000000_requests.up.fizz @@ -0,0 +1,55 @@ +create_table("selfservice_login_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + + t.Timestamps() +} + +create_table("selfservice_login_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_login_request_id", "uuid") + t.Column("config", "json") + + t.Timestamps() + + t.ForeignKey("selfservice_login_request_id", {"selfservice_login_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_registration_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("active_method", "string", {"size": 32}) + + t.Timestamps() +} + +create_table("selfservice_registration_request_methods") { + t.Column("id", "uuid", {primary: true}) + t.Column("method", "string", {"size": 32}) + t.Column("selfservice_registration_request_id", "uuid") + t.Column("config", "json") + + t.Timestamps() + + t.ForeignKey("selfservice_registration_request_id", {"selfservice_registration_requests": ["id"]}, {"on_delete": "cascade"}) +} + +create_table("selfservice_profile_management_requests") { + t.Column("id", "uuid", {primary: true}) + t.Column("request_url", "string", {"size": 2048}) + t.Column("issued_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("form", "json") + t.Column("update_successful", "bool") + t.Column("identity_id", "uuid") + + t.Timestamps() + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/contrib/sql/migrations/20191122000000_sessions.down.fizz b/contrib/sql/migrations/20191122000000_sessions.down.fizz new file mode 100644 index 00000000000..dc5c982c81f --- /dev/null +++ b/contrib/sql/migrations/20191122000000_sessions.down.fizz @@ -0,0 +1 @@ +drop_table("sessions") diff --git a/contrib/sql/migrations/20191122000000_sessions.up.fizz b/contrib/sql/migrations/20191122000000_sessions.up.fizz new file mode 100644 index 00000000000..94ab9a4e143 --- /dev/null +++ b/contrib/sql/migrations/20191122000000_sessions.up.fizz @@ -0,0 +1,11 @@ +create_table("sessions") { + t.Column("id", "uuid", {primary: true}) + t.Column("issued_at", "timestamp", { default_raw: "CURRENT_TIMESTAMP" }) + t.Column("expires_at", "timestamp") + t.Column("authenticated_at", "timestamp") + t.Column("identity_id", "uuid") + + t.Timestamps() + + t.ForeignKey("identity_id", {"identities": ["id"]}, {"on_delete": "cascade"}) +} diff --git a/contrib/sql/migrations/20191123000000_errors.down.fizz b/contrib/sql/migrations/20191123000000_errors.down.fizz new file mode 100644 index 00000000000..9ada90a727b --- /dev/null +++ b/contrib/sql/migrations/20191123000000_errors.down.fizz @@ -0,0 +1 @@ +drop_table("selfservice_errors") diff --git a/contrib/sql/migrations/20191123000000_errors.up.fizz b/contrib/sql/migrations/20191123000000_errors.up.fizz new file mode 100644 index 00000000000..e17347c2692 --- /dev/null +++ b/contrib/sql/migrations/20191123000000_errors.up.fizz @@ -0,0 +1,8 @@ +create_table("selfservice_errors") { + t.Column("id", "uuid", {primary: true}) + t.Column("errors", "json") + t.Column("seen_at", "timestamp") + t.Column("was_seen", "bool") + + t.Timestamps() +} diff --git a/contrib/sql/migrations/20191130170530_identities.mysql.down.sql b/contrib/sql/migrations/20191130170530_identities.mysql.down.sql new file mode 100644 index 00000000000..e69de29bb2d diff --git a/contrib/sql/migrations/20191130170530_identities.mysql.up.sql b/contrib/sql/migrations/20191130170530_identities.mysql.up.sql new file mode 100644 index 00000000000..8069ee98f31 --- /dev/null +++ b/contrib/sql/migrations/20191130170530_identities.mysql.up.sql @@ -0,0 +1 @@ +ALTER TABLE identity_credential_identifiers MODIFY COLUMN identifier VARCHAR(255) BINARY; diff --git a/contrib/sql/migrations/postgres/1.sql b/contrib/sql/migrations/postgres/1.sql deleted file mode 100644 index 7d83925c0ba..00000000000 --- a/contrib/sql/migrations/postgres/1.sql +++ /dev/null @@ -1,61 +0,0 @@ --- +migrate Up -CREATE TYPE credentials_type AS ENUM ('oidc', 'password'); -CREATE TYPE self_service_request_type AS ENUM ('login', 'registration'); - -CREATE TABLE IF NOT EXISTS identity -( - pk BIGSERIAL PRIMARY KEY, - id VARCHAR(255) NOT NULL UNIQUE, - traits jsonb NOT NULL DEFAULT '{}'::jsonb, - traits_schema_url text NOT NULL -); - -CREATE TABLE IF NOT EXISTS identity_credential -( - pk BIGSERIAL PRIMARY KEY, - identity_pk BIGINT REFERENCES identity (pk) ON DELETE CASCADE, - method credentials_type NOT NULL, - config jsonb NOT NULL DEFAULT '{}'::jsonb -); - -CREATE TABLE IF NOT EXISTS identity_credential_identifier -( - pk BIGSERIAL PRIMARY KEY, - identity_credential_pk BIGINT REFERENCES identity_credential (pk) ON DELETE CASCADE, - identifier VARCHAR(255) NOT NULL UNIQUE, - CHECK (length(identifier) > 0) -); - -CREATE TABLE IF NOT EXISTS self_service_request -( - pk BIGSERIAL PRIMARY KEY, - id VARCHAR(36) NOT NULL UNIQUE, - expires_at TIMESTAMP WITH TIME ZONE NOT NULL, - issued_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - request_url text NOT NULL, - request_headers jsonb NOT NULL DEFAULT '{}'::jsonb, - active credentials_type NULL, - methods jsonb NOT NULL DEFAULT '{}'::jsonb, - kind self_service_request_type NOT NULL -); - -CREATE TABLE IF NOT EXISTS session -( - pk BIGSERIAL PRIMARY KEY, - sid VARCHAR(36) NOT NULL UNIQUE, - expires_at TIMESTAMP WITH TIME ZONE NOT NULL, - issued_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - authenticated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), - identity_pk BIGINT REFERENCES identity (pk) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX name ON self_service_request (id, kind); - --- +migrate Down -DROP TABLE identity_credential_identifier; -DROP TABLE identity_credential; -DROP TABLE session; -DROP TABLE identity; -DROP TABLE self_service_request; -DROP TYPE IF EXISTS credentials_type; -DROP TYPE IF EXISTS self_service_request_type; diff --git a/contrib/sql/migrations/postgres/2.sql b/contrib/sql/migrations/postgres/2.sql deleted file mode 100644 index cfd157adbc7..00000000000 --- a/contrib/sql/migrations/postgres/2.sql +++ /dev/null @@ -1,12 +0,0 @@ --- +migrate Up -CREATE TABLE IF NOT EXISTS self_service_error -( - pk BIGSERIAL PRIMARY KEY, - id UUID NOT NULL UNIQUE, - errors jsonb NOT NULL, - seen_at TIMESTAMP WITH TIME ZONE NULL, - was_seen BOOL NOT NULL DEFAULT false -); - --- +migrate Down -DROP TABLE self_service_error; diff --git a/contrib/sql/migrations/tests/1_test.sql b/contrib/sql/migrations/tests/1_test.sql deleted file mode 100644 index 0da70a54332..00000000000 --- a/contrib/sql/migrations/tests/1_test.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +migrate Up -INSERT INTO identity (id, traits_schema_url) -VALUES ('data-1', 'foo'); - -INSERT INTO identity_credential (identity_pk, method, config) -VALUES (1, 'password', '{"foo":"bar"}'); - -INSERT INTO identity_credential_identifier (identity_credential_pk, identifier) -VALUES (1, 'data-1@example.org'); - -INSERT INTO identity_credential_identifier (identity_credential_pk, identifier) -VALUES (1, 'data-1@example.com'); - -INSERT INTO self_service_request (id, expires_at, issued_at, request_url, request_headers, active, methods, kind) -VALUES (1, NOW(), NOW(), 'https://www.ory.sh/', '{}', 'password', '{}', 'login'); - -INSERT INTO session (sid, expires_at, issued_at, authenticated_at, identity_pk) -VALUES ('sid-1', NOW(), NOW(), NOW(), 1); - --- +migrate Down diff --git a/contrib/sql/migrations/tests/2_test.sql b/contrib/sql/migrations/tests/2_test.sql deleted file mode 100644 index 8db85019481..00000000000 --- a/contrib/sql/migrations/tests/2_test.sql +++ /dev/null @@ -1,36 +0,0 @@ --- +migrate Up -INSERT INTO identity (id, traits_schema_url) -VALUES ('data-2', 'foo'); - -INSERT INTO identity_credential (identity_pk, method, config) -VALUES (2, 'password', '{"foo":"bar"}'); - -INSERT INTO identity_credential_identifier (identity_credential_pk, identifier) -VALUES (2, 'data-2@example.org'); - -INSERT INTO identity_credential_identifier (identity_credential_pk, identifier) -VALUES (2, 'data-2@example.com'); - -INSERT INTO self_service_request (id, expires_at, issued_at, request_url, request_headers, active, methods, kind) -VALUES (2, NOW(), NOW(), 'https://www.ory.sh/', '{}', 'password', '{}', 'login'); - -INSERT INTO session (sid, expires_at, issued_at, authenticated_at, identity_pk) -VALUES ('sid-2', NOW(), NOW(), NOW(), 2); - -INSERT INTO self_service_error (id, errors, seen_at, was_seen) -VALUES ('2222-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', '[ - "foo", - { - "name": "bar" - } -]', NULL, false); - -INSERT INTO self_service_error (id, errors, seen_at, was_seen) -VALUES ('2223-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', '[ - "foo", - { - "name": "bar" - } -]', NOW(), true); - --- +migrate Down diff --git a/contrib/swagutil/cmd/sanitize.go b/contrib/swagutil/cmd/sanitize.go index 02f473b6fdf..0c37a133a3e 100644 --- a/contrib/swagutil/cmd/sanitize.go +++ b/contrib/swagutil/cmd/sanitize.go @@ -46,6 +46,8 @@ var sanitizeCmd = &cobra.Command{ cmdx.Must(err, "Unable to read file: %s", err) result := []byte(sanitizeIter(string(file))) + result, err = sjson.SetRawBytes(result, "definitions.UUID", []byte(`{"type": "string", "format": "uuid4"}`)) + cmdx.Must(err, "could not set definitions.UUID: %s", err) err = os.Remove(args[0]) cmdx.Must(err, "Unable to delete file: %s", err) diff --git a/docs/api.swagger.json b/docs/api.swagger.json index 4d36bb7ce44..e3642998b56 100755 --- a/docs/api.swagger.json +++ b/docs/api.swagger.json @@ -318,9 +318,6 @@ } }, "definitions": { - "CSRFSetter": { - "type": "object" - }, "CredentialsType": { "description": "and so on.", "type": "string", @@ -335,11 +332,35 @@ } } }, + "Identity": { + "type": "object", + "required": [ + "id", + "traits" + ], + "properties": { + "id": { + "$ref": "#/definitions/UUID" + }, + "traits": { + "$ref": "#/definitions/Traits" + }, + "traits_schema_url": { + "description": "TraitsSchemaURL is the JSON Schema to be used for validating the identity's traits.\n\nformat: uri", + "type": "string" + } + } + }, "RWMutex": { "description": "A RWMutex must not be copied after first use.\n\nIf a goroutine holds a RWMutex for reading and another goroutine might\ncall Lock, no goroutine should expect to be able to acquire a read lock\nuntil the initial read lock is released. In particular, this prohibits\nrecursive read locking. This is to ensure that the lock eventually becomes\navailable; a blocked Lock call excludes new readers from acquiring the\nlock.", "type": "object", "title": "A RWMutex is a reader/writer mutual exclusion lock.\nThe lock can be held by an arbitrary number of readers or a single writer.\nThe zero value for a RWMutex is an unlocked mutex." }, + "RequestMethodConfigurator": {}, + "Traits": { + "type": "object" + }, + "UUID": {"type": "string", "format": "uuid4"}, "form": { "description": "HTMLForm represents a HTML Form. The container can work with both HTTP Form and JSON requests", "type": "object", @@ -453,56 +474,6 @@ } } }, - "identity": { - "description": "An identity can be a real human, a service, an IoT device - everything that\ncan be described as an \"actor\" in a system.", - "type": "object", - "title": "Identity represents an ORY Kratos identity", - "required": [ - "id", - "traits" - ], - "properties": { - "credentials": { - "description": "Credentials represents all credentials that can be used for authenticating this identity.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/identityCredentials" - } - }, - "id": { - "description": "ID is a unique identifier chosen by you. It can be a URN (e.g. \"arn:aws:iam::123456789012\"),\na stringified integer (e.g. \"123456789012\"), a uuid (e.g. \"9f425a8d-7efc-4768-8f23-7647a74fdf13\"). It is up to you\nto pick a format you'd like. It is discouraged to use a personally identifiable value here, like the username\nor the email, as this field is immutable.", - "type": "string" - }, - "traits": { - "description": "Traits represent an identity's traits. The identity is able to create, modify, and delete traits\nin a self-service manner. The input will always be validated against the JSON Schema defined\nin `traits_schema_url`.", - "type": "object" - }, - "traits_schema_url": { - "description": "TraitsSchemaURL is the JSON Schema to be used for validating the identity's traits.\n\nformat: uri", - "type": "string" - } - } - }, - "identityCredentials": { - "description": "Credentials represents a specific credential type", - "type": "object", - "properties": { - "config": { - "description": "Config contains the concrete credential payload. This might contain the bcrypt-hashed password, or the email\nfor passwordless authentication.\n\ntype: string\nformat: binary", - "type": "object" - }, - "id": { - "$ref": "#/definitions/CredentialsType" - }, - "identifiers": { - "description": "Identifiers represents a list of unique identifiers this credential type matches.", - "type": "array", - "items": { - "type": "string" - } - } - } - }, "loginRequest": { "type": "object", "properties": { @@ -515,8 +486,7 @@ "format": "date-time" }, "id": { - "description": "ID represents the request's unique ID. When performing the login flow, this\nrepresents the id in the login ui's query parameter: http://\u003curls.login_ui\u003e/?request=\u003cid\u003e", - "type": "string" + "$ref": "#/definitions/UUID" }, "issued_at": { "description": "IssuedAt is the time (UTC) when the request occurred.", @@ -618,11 +588,10 @@ "$ref": "#/definitions/form" }, "id": { - "description": "ID represents the request's unique ID. When performing the profile management flow, this\nrepresents the id in the profile ui's query parameter: http://\u003curls.profile_ui\u003e?request=\u003cid\u003e", - "type": "string" + "$ref": "#/definitions/UUID" }, "identity": { - "$ref": "#/definitions/identity" + "$ref": "#/definitions/Identity" }, "issued_at": { "description": "IssuedAt is the time (UTC) when the request occurred.", @@ -651,8 +620,7 @@ "format": "date-time" }, "id": { - "description": "ID represents the request's unique ID. When performing the registration flow, this\nrepresents the id in the registration ui's query parameter: http://registration-ui/?request=\u003cid\u003e", - "type": "string" + "$ref": "#/definitions/UUID" }, "issued_at": { "description": "IssuedAt is the time (UTC) when the request occurred.", diff --git a/driver/configuration/provider_viper_test.go b/driver/configuration/provider_viper_test.go index 33bb4396bc9..b4af1cde42f 100644 --- a/driver/configuration/provider_viper_test.go +++ b/driver/configuration/provider_viper_test.go @@ -4,11 +4,12 @@ import ( "testing" "time" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ory/x/errorsx" + "github.com/ory/viper" "github.com/ory/x/viperx" @@ -30,7 +31,7 @@ func TestViperProvider(t *testing.T) { viperx.LoggerWithValidationErrorFields(logrus.New(), err).Error(err.Error()) } - require.NoError(t, err, "%+v", errors.Cause(err)) + require.NoError(t, err, "%+v", errorsx.Cause(err)) p := NewViperProvider(logrus.New()) t.Run("group=urls", func(t *testing.T) { diff --git a/driver/driver_default.go b/driver/driver_default.go index e8114fa1f94..0f53148f1e4 100644 --- a/driver/driver_default.go +++ b/driver/driver_default.go @@ -1,6 +1,7 @@ package driver import ( + "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/ory/x/logrusx" @@ -13,7 +14,7 @@ type DefaultDriver struct { r Registry } -func NewDefaultDriver(l logrus.FieldLogger, version, build, date string) Driver { +func NewDefaultDriver(l logrus.FieldLogger, version, build, date string) (Driver, error) { if l == nil { l = logrusx.New() } @@ -22,7 +23,7 @@ func NewDefaultDriver(l logrus.FieldLogger, version, build, date string) Driver r, err := NewRegistry(c) if err != nil { - l.WithError(err).Fatal("Unable to instantiate service registry.") + return nil, errors.Wrap(err, "unable to instantiate service registry") } r. @@ -32,10 +33,18 @@ func NewDefaultDriver(l logrus.FieldLogger, version, build, date string) Driver // Init forces the driver to initialize and circumvent lazy loading issues. if err = r.Init(); err != nil { - l.WithError(err).Fatal("Unable to initialize service registry.") + return nil, errors.Wrap(err, "unable to initialize service registry") } - return &DefaultDriver{r: r, c: c} + return &DefaultDriver{r: r, c: c}, nil +} + +func MustNewDefaultDriver(l logrus.FieldLogger, version, build, date string) Driver { + d, err := NewDefaultDriver(l, version, build, date) + if err != nil { + l.WithError(err).Fatal("Unable to initialize driver.") + } + return d } func (r *DefaultDriver) BuildInfo() *BuildInfo { diff --git a/driver/registry.go b/driver/registry.go index cd0de5dc8cd..24f6a5db95e 100644 --- a/driver/registry.go +++ b/driver/registry.go @@ -8,6 +8,7 @@ import ( "github.com/ory/x/healthx" + "github.com/ory/kratos/persistence" "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/logout" "github.com/ory/kratos/selfservice/flow/profile" @@ -45,8 +46,11 @@ type Registry interface { x.WriterProvider x.LoggingProvider + persistence.Provider + errorx.ManagementProvider errorx.HandlerProvider + errorx.PersistenceProvider identity.HandlerProvider identity.ValidationProvider @@ -57,6 +61,7 @@ type Registry interface { session.HandlerProvider session.ManagementProvider + session.PersistenceProvider profile.HandlerProvider profile.ErrorHandlerProvider diff --git a/driver/registry_abstract.go b/driver/registry_abstract.go deleted file mode 100644 index 8b7c9e8b619..00000000000 --- a/driver/registry_abstract.go +++ /dev/null @@ -1,318 +0,0 @@ -package driver - -import ( - "bytes" - "encoding/json" - "fmt" - "net/url" - - "github.com/gorilla/sessions" - "github.com/justinas/nosurf" - "github.com/sirupsen/logrus" - - "github.com/ory/x/healthx" - - "github.com/ory/x/tracing" - - "github.com/ory/x/logrusx" - - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/logout" - "github.com/ory/kratos/selfservice/flow/profile" - "github.com/ory/kratos/selfservice/flow/registration" - "github.com/ory/kratos/selfservice/hook" - - "github.com/ory/kratos/selfservice/strategy/oidc" - - "github.com/ory/herodot" - - "github.com/ory/kratos/driver/configuration" - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/errorx" - password2 "github.com/ory/kratos/selfservice/strategy/password" - "github.com/ory/kratos/session" -) - -type RegistryAbstract struct { - l logrus.FieldLogger - c configuration.Provider - nosurf *nosurf.CSRFHandler - trc *tracing.Tracer - writer herodot.Writer - healthxHandler *healthx.Handler - - identityHandler *identity.Handler - identityValidator *identity.Validator - sessionHandler *session.Handler - errorHandler *errorx.Handler - passwordHasher password2.Hasher - passwordValidator password2.Validator - sessionsStore sessions.Store - - selfserviceRegistrationExecutor *registration.HookExecutor - selfserviceRegistrationHandler *registration.Handler - seflserviceRegistrationErrorHandler *registration.ErrorHandler - selfserviceRegistrationRequestErrorHandler *registration.ErrorHandler - - selfserviceLoginExecutor *login.HookExecutor - selfserviceLoginHandler *login.Handler - selfserviceLoginRequestErrorHandler *login.ErrorHandler - - selfserviceProfileManagementHandler *profile.Handler - selfserviceProfileRequestRequestErrorHandler *profile.ErrorHandler - - selfserviceLogoutHandler *logout.Handler - - selfserviceStrategies []selfServiceStrategy - - buildVersion string - buildHash string - buildDate string - - r Registry -} - -func (m *RegistryAbstract) WithBuildInfo(version, hash, date string) Registry { - m.buildVersion = version - m.buildHash = hash - m.buildDate = date - return m.r -} - -func (m *RegistryAbstract) BuildVersion() string { - return m.buildVersion -} - -func (m *RegistryAbstract) BuildDate() string { - return m.buildDate -} - -func (m *RegistryAbstract) BuildHash() string { - return m.buildHash -} - -func (m *RegistryAbstract) with(r Registry) *RegistryAbstract { - m.r = r - return m -} - -func (m *RegistryAbstract) WithLogger(l logrus.FieldLogger) Registry { - m.l = l - return m.r -} - -func (m *RegistryAbstract) ProfileManagementHandler() *profile.Handler { - if m.selfserviceProfileManagementHandler == nil { - m.selfserviceProfileManagementHandler = profile.NewHandler(m.r, m.c) - } - return m.selfserviceProfileManagementHandler -} - -func (m *RegistryAbstract) ProfileRequestRequestErrorHandler() *profile.ErrorHandler { - if m.selfserviceProfileRequestRequestErrorHandler == nil { - m.selfserviceProfileRequestRequestErrorHandler = profile.NewErrorHandler(m.r, m.c) - } - return m.selfserviceProfileRequestRequestErrorHandler -} - -func (m *RegistryAbstract) LogoutHandler() *logout.Handler { - if m.selfserviceLogoutHandler == nil { - m.selfserviceLogoutHandler = logout.NewHandler(m.r, m.c) - } - return m.selfserviceLogoutHandler -} - -func (m *RegistryAbstract) HealthHandler() *healthx.Handler { - if m.healthxHandler == nil { - m.healthxHandler = healthx.NewHandler(m.Writer(), m.BuildVersion(), healthx.ReadyCheckers{ - "database": m.r.Ping, - }) - } - - return m.healthxHandler -} - -func (m *RegistryAbstract) WithCSRFHandler(c *nosurf.CSRFHandler) { - m.nosurf = c -} - -func (m *RegistryAbstract) CSRFHandler() *nosurf.CSRFHandler { - if m.nosurf == nil { - panic("csrf handler is not set") - } - return m.nosurf -} - -func (m *RegistryAbstract) selfServiceStrategies() []selfServiceStrategy { - if m.selfserviceStrategies == nil { - m.selfserviceStrategies = []selfServiceStrategy{ - password2.NewStrategy(m.r, m.c), - oidc.NewStrategy(m.r, m.c), - } - } - - return m.selfserviceStrategies -} - -func (m *RegistryAbstract) RegistrationStrategies() registration.Strategies { - strategies := make([]registration.Strategy, len(m.selfServiceStrategies())) - for i := range strategies { - strategies[i] = m.selfServiceStrategies()[i] - } - return strategies -} - -func (m *RegistryAbstract) LoginStrategies() login.Strategies { - strategies := make([]login.Strategy, len(m.selfServiceStrategies())) - for i := range strategies { - strategies[i] = m.selfServiceStrategies()[i] - } - return strategies -} - -func (m *RegistryAbstract) hooksPost(credentialsType identity.CredentialsType, configs []configuration.SelfServiceHook) postHooks { - var i postHooks - - for _, h := range configs { - switch h.Run { - case hook.KeySessionIssuer: - i = append( - i, - hook.NewSessionIssuer(m.r), - ) - case hook.KeyRedirector: - var rc struct { - R string `json:"default_redirect_url"` - A bool `json:"allow_user_defined_redirect"` - } - - if err := json.NewDecoder(bytes.NewBuffer(h.Config)).Decode(&rc); err != nil { - m.l.WithError(err). - WithField("type", credentialsType). - WithField("hook", h.Run). - WithField("config", fmt.Sprintf("%s", h.Config)). - Errorf("The after hook is misconfigured.") - continue - } - - rcr, err := url.ParseRequestURI(rc.R) - if err != nil { - m.l.WithError(err). - WithField("type", credentialsType). - WithField("hook", h.Run). - WithField("config", fmt.Sprintf("%s", h.Config)). - Errorf("The after hook is misconfigured.") - continue - } - - i = append( - i, - hook.NewRedirector( - func() *url.URL { - return rcr - }, - m.c.WhitelistedReturnToDomains, - func() bool { - return rc.A - }, - ), - ) - default: - m.l. - WithField("type", credentialsType). - WithField("hook", h.Run). - Errorf("A unknown post login hook was requested and can therefore not be used.") - } - } - - return i -} - -func (m *RegistryAbstract) IdentityValidator() *identity.Validator { - if m.identityValidator == nil { - m.identityValidator = identity.NewValidator(m.c) - } - return m.identityValidator -} - -func (m *RegistryAbstract) WithConfig(c configuration.Provider) Registry { - m.c = c - return m.r -} - -func (m *RegistryAbstract) Writer() herodot.Writer { - if m.writer == nil { - h := herodot.NewJSONWriter(m.Logger()) - m.writer = h - } - return m.writer -} - -func (m *RegistryAbstract) Logger() logrus.FieldLogger { - if m.l == nil { - m.l = logrusx.New() - } - return m.l -} - -func (m *RegistryAbstract) IdentityHandler() *identity.Handler { - if m.identityHandler == nil { - m.identityHandler = identity.NewHandler(m.c, m.r) - } - return m.identityHandler -} - -func (m *RegistryAbstract) SessionHandler() *session.Handler { - if m.sessionHandler == nil { - m.sessionHandler = session.NewHandler(m.r, m.Writer()) - } - return m.sessionHandler -} - -func (m *RegistryAbstract) PasswordHasher() password2.Hasher { - if m.passwordHasher == nil { - m.passwordHasher = password2.NewHasherArgon2(m.c) - } - return m.passwordHasher -} - -func (m *RegistryAbstract) PasswordValidator() password2.Validator { - if m.passwordValidator == nil { - m.passwordValidator = password2.NewDefaultPasswordValidatorStrategy() - } - return m.passwordValidator -} - -func (m *RegistryAbstract) SelfServiceErrorHandler() *errorx.Handler { - if m.errorHandler == nil { - m.errorHandler = errorx.NewHandler(m.r) - } - return m.errorHandler -} - -func (m *RegistryAbstract) CookieManager() sessions.Store { - if m.sessionsStore == nil { - cs := sessions.NewCookieStore(m.c.SessionSecrets()...) - cs.Options.Secure = m.c.SelfPublicURL().Scheme == "https" - cs.Options.HttpOnly = true - m.sessionsStore = cs - } - return m.sessionsStore -} - -func (m *RegistryAbstract) Tracer() *tracing.Tracer { - if m.trc == nil { - m.trc = &tracing.Tracer{ - ServiceName: m.c.TracingServiceName(), - JaegerConfig: m.c.TracingJaegerConfig(), - Provider: m.c.TracingProvider(), - Logger: m.Logger(), - } - - if err := m.trc.Setup(); err != nil { - m.Logger().WithError(err).Fatalf("Unable to initialize Tracer.") - } - } - - return m.trc -} diff --git a/driver/registry_default.go b/driver/registry_default.go new file mode 100644 index 00000000000..cbcb704c811 --- /dev/null +++ b/driver/registry_default.go @@ -0,0 +1,372 @@ +package driver + +import ( + "context" + "strings" + "time" + + "github.com/cenkalti/backoff" + "github.com/gobuffalo/pop" + "github.com/gorilla/sessions" + "github.com/justinas/nosurf" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/ory/x/dbal" + "github.com/ory/x/healthx" + "github.com/ory/x/sqlcon" + + "github.com/ory/x/tracing" + + "github.com/ory/x/logrusx" + + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/persistence/sql" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/logout" + "github.com/ory/kratos/selfservice/flow/profile" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/selfservice/strategy/oidc" + + "github.com/ory/herodot" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + password2 "github.com/ory/kratos/selfservice/strategy/password" + "github.com/ory/kratos/session" +) + +var _ Registry = new(RegistryDefault) + +func init() { + dbal.RegisterDriver(NewRegistryDefault()) +} + +type RegistryDefault struct { + l logrus.FieldLogger + c configuration.Provider + nosurf *nosurf.CSRFHandler + trc *tracing.Tracer + writer herodot.Writer + healthxHandler *healthx.Handler + + persister persistence.Persister + + identityHandler *identity.Handler + identityValidator *identity.Validator + + sessionHandler *session.Handler + sessionsStore sessions.Store + sessionManager session.Manager + + passwordHasher password2.Hasher + passwordValidator password2.Validator + + errorHandler *errorx.Handler + errorManager *errorx.Manager + + selfserviceRegistrationExecutor *registration.HookExecutor + selfserviceRegistrationHandler *registration.Handler + seflserviceRegistrationErrorHandler *registration.ErrorHandler + selfserviceRegistrationRequestErrorHandler *registration.ErrorHandler + + selfserviceLoginExecutor *login.HookExecutor + selfserviceLoginHandler *login.Handler + selfserviceLoginRequestErrorHandler *login.ErrorHandler + + selfserviceProfileManagementHandler *profile.Handler + selfserviceProfileRequestRequestErrorHandler *profile.ErrorHandler + + selfserviceLogoutHandler *logout.Handler + + selfserviceStrategies []selfServiceStrategy + + buildVersion string + buildHash string + buildDate string +} + +func NewRegistryDefault() *RegistryDefault { + return &RegistryDefault{} +} + +func (m *RegistryDefault) WithBuildInfo(version, hash, date string) Registry { + m.buildVersion = version + m.buildHash = hash + m.buildDate = date + return m +} + +func (m *RegistryDefault) BuildVersion() string { + return m.buildVersion +} + +func (m *RegistryDefault) BuildDate() string { + return m.buildDate +} + +func (m *RegistryDefault) BuildHash() string { + return m.buildHash +} + +func (m *RegistryDefault) WithLogger(l logrus.FieldLogger) Registry { + m.l = l + return m +} + +func (m *RegistryDefault) ProfileManagementHandler() *profile.Handler { + if m.selfserviceProfileManagementHandler == nil { + m.selfserviceProfileManagementHandler = profile.NewHandler(m, m.c) + } + return m.selfserviceProfileManagementHandler +} + +func (m *RegistryDefault) ProfileRequestRequestErrorHandler() *profile.ErrorHandler { + if m.selfserviceProfileRequestRequestErrorHandler == nil { + m.selfserviceProfileRequestRequestErrorHandler = profile.NewErrorHandler(m, m.c) + } + return m.selfserviceProfileRequestRequestErrorHandler +} + +func (m *RegistryDefault) LogoutHandler() *logout.Handler { + if m.selfserviceLogoutHandler == nil { + m.selfserviceLogoutHandler = logout.NewHandler(m, m.c) + } + return m.selfserviceLogoutHandler +} + +func (m *RegistryDefault) HealthHandler() *healthx.Handler { + if m.healthxHandler == nil { + m.healthxHandler = healthx.NewHandler(m.Writer(), m.BuildVersion(), healthx.ReadyCheckers{ + "database": m.Ping, + }) + } + + return m.healthxHandler +} + +func (m *RegistryDefault) WithCSRFHandler(c *nosurf.CSRFHandler) { + m.nosurf = c +} + +func (m *RegistryDefault) CSRFHandler() *nosurf.CSRFHandler { + if m.nosurf == nil { + panic("csrf handler is not set") + } + return m.nosurf +} + +func (m *RegistryDefault) selfServiceStrategies() []selfServiceStrategy { + if m.selfserviceStrategies == nil { + m.selfserviceStrategies = []selfServiceStrategy{ + password2.NewStrategy(m, m.c), + oidc.NewStrategy(m, m.c), + } + } + + return m.selfserviceStrategies +} + +func (m *RegistryDefault) RegistrationStrategies() registration.Strategies { + strategies := make([]registration.Strategy, len(m.selfServiceStrategies())) + for i := range strategies { + strategies[i] = m.selfServiceStrategies()[i] + } + return strategies +} + +func (m *RegistryDefault) LoginStrategies() login.Strategies { + strategies := make([]login.Strategy, len(m.selfServiceStrategies())) + for i := range strategies { + strategies[i] = m.selfServiceStrategies()[i] + } + return strategies +} + +func (m *RegistryDefault) IdentityValidator() *identity.Validator { + if m.identityValidator == nil { + m.identityValidator = identity.NewValidator(m.c) + } + return m.identityValidator +} + +func (m *RegistryDefault) WithConfig(c configuration.Provider) Registry { + m.c = c + return m +} + +func (m *RegistryDefault) Writer() herodot.Writer { + if m.writer == nil { + h := herodot.NewJSONWriter(m.Logger()) + m.writer = h + } + return m.writer +} + +func (m *RegistryDefault) Logger() logrus.FieldLogger { + if m.l == nil { + m.l = logrusx.New() + } + return m.l +} + +func (m *RegistryDefault) IdentityHandler() *identity.Handler { + if m.identityHandler == nil { + m.identityHandler = identity.NewHandler(m.c, m) + } + return m.identityHandler +} + +func (m *RegistryDefault) SessionHandler() *session.Handler { + if m.sessionHandler == nil { + m.sessionHandler = session.NewHandler(m) + } + return m.sessionHandler +} + +func (m *RegistryDefault) PasswordHasher() password2.Hasher { + if m.passwordHasher == nil { + m.passwordHasher = password2.NewHasherArgon2(m.c) + } + return m.passwordHasher +} + +func (m *RegistryDefault) PasswordValidator() password2.Validator { + if m.passwordValidator == nil { + m.passwordValidator = password2.NewDefaultPasswordValidatorStrategy() + } + return m.passwordValidator +} + +func (m *RegistryDefault) SelfServiceErrorHandler() *errorx.Handler { + if m.errorHandler == nil { + m.errorHandler = errorx.NewHandler(m) + } + return m.errorHandler +} + +func (m *RegistryDefault) CookieManager() sessions.Store { + if m.sessionsStore == nil { + cs := sessions.NewCookieStore(m.c.SessionSecrets()...) + cs.Options.Secure = m.c.SelfPublicURL().Scheme == "https" + cs.Options.HttpOnly = true + m.sessionsStore = cs + } + return m.sessionsStore +} + +func (m *RegistryDefault) Tracer() *tracing.Tracer { + if m.trc == nil { + m.trc = &tracing.Tracer{ + ServiceName: m.c.TracingServiceName(), + JaegerConfig: m.c.TracingJaegerConfig(), + Provider: m.c.TracingProvider(), + Logger: m.Logger(), + } + + if err := m.trc.Setup(); err != nil { + m.Logger().WithError(err).Fatalf("Unable to initialize Tracer.") + } + } + + return m.trc +} + +func (m *RegistryDefault) SessionManager() session.Manager { + if m.sessionManager == nil { + m.sessionManager = session.NewManagerHTTP(m.c, m) + } + return m.sessionManager +} + +func (m *RegistryDefault) SelfServiceErrorManager() *errorx.Manager { + if m.errorManager == nil { + m.errorManager = errorx.NewManager(m, m.c) + } + return m.errorManager +} + +func (m *RegistryDefault) CanHandle(dsn string) bool { + return dsn == "memory" || + strings.HasPrefix(dsn, "mysql") || + strings.HasPrefix(dsn, "sqlite") || + strings.HasPrefix(dsn, "sqlite3") || + strings.HasPrefix(dsn, "postgres") || + strings.HasPrefix(dsn, "postgresql") || + strings.HasPrefix(dsn, "cockroach") || + strings.HasPrefix(dsn, "cockroachdb") || + strings.HasPrefix(dsn, "crdb") +} + +func (m *RegistryDefault) Init() error { + if m.persister != nil { + return nil + } + + bc := backoff.NewExponentialBackOff() + bc.MaxElapsedTime = time.Minute * 5 + bc.Reset() + return errors.WithStack( + backoff.Retry(func() error { + pool, idlePool, connMaxLifetime := sqlcon.ParseConnectionOptions(m.l, m.c.DSN()) + c, err := pop.NewConnection(&pop.ConnectionDetails{ + URL: m.c.DSN(), + IdlePool: idlePool, + ConnMaxLifetime: connMaxLifetime, + Pool: pool, + }) + if err != nil { + m.Logger().WithError(err).Warnf("Unable to connect to database, retrying.") + return errors.WithStack(err) + } + if err := c.Open(); err != nil { + m.Logger().WithError(err).Warnf("Unable to open database, retrying.") + return errors.WithStack(err) + } + p, err := sql.NewPersister(m, m.c, c) + if err != nil { + m.Logger().WithError(err).Warnf("Unable to initialize persister, retrying.") + return err + } + if err := p.Ping(context.Background()); err != nil { + m.Logger().WithError(err).Warnf("Unable to ping database, retrying.") + return err + } + m.persister = p + return nil + }, bc), + ) +} + +func (m *RegistryDefault) IdentityPool() identity.Pool { + return m.persister +} + +func (m *RegistryDefault) RegistrationRequestPersister() registration.RequestPersister { + return m.persister +} + +func (m *RegistryDefault) LoginRequestPersister() login.RequestPersister { + return m.persister +} + +func (m *RegistryDefault) ProfileRequestPersister() profile.RequestPersister { + return m.persister +} + +func (m *RegistryDefault) SelfServiceErrorPersister() errorx.Persister { + return m.persister +} + +func (m *RegistryDefault) SessionPersister() session.Persister { + return m.persister +} + +func (m *RegistryDefault) Persister() persistence.Persister { + return m.persister +} + +func (m *RegistryDefault) Ping() error { + return m.persister.Ping(context.Background()) +} diff --git a/driver/registry_default_hooks.go b/driver/registry_default_hooks.go new file mode 100644 index 00000000000..13a664ef1d1 --- /dev/null +++ b/driver/registry_default_hooks.go @@ -0,0 +1,70 @@ +package driver + +import ( + "bytes" + "encoding/json" + "fmt" + "net/url" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/hook" +) + +func (m *RegistryDefault) hooksPost(credentialsType identity.CredentialsType, configs []configuration.SelfServiceHook) postHooks { + var i postHooks + + for _, h := range configs { + switch h.Run { + case hook.KeySessionIssuer: + i = append( + i, + hook.NewSessionIssuer(m), + ) + case hook.KeyRedirector: + var rc struct { + R string `json:"default_redirect_url"` + A bool `json:"allow_user_defined_redirect"` + } + + if err := json.NewDecoder(bytes.NewBuffer(h.Config)).Decode(&rc); err != nil { + m.l.WithError(err). + WithField("type", credentialsType). + WithField("hook", h.Run). + WithField("config", fmt.Sprintf("%s", h.Config)). + Errorf("The after hook is misconfigured.") + continue + } + + rcr, err := url.ParseRequestURI(rc.R) + if err != nil { + m.l.WithError(err). + WithField("type", credentialsType). + WithField("hook", h.Run). + WithField("config", fmt.Sprintf("%s", h.Config)). + Errorf("The after hook is misconfigured.") + continue + } + + i = append( + i, + hook.NewRedirector( + func() *url.URL { + return rcr + }, + m.c.WhitelistedReturnToDomains, + func() bool { + return rc.A + }, + ), + ) + default: + m.l. + WithField("type", credentialsType). + WithField("hook", h.Run). + Errorf("A unknown post login hook was requested and can therefore not be used.") + } + } + + return i +} diff --git a/driver/registry_abstract_login.go b/driver/registry_default_login.go similarity index 56% rename from driver/registry_abstract_login.go rename to driver/registry_default_login.go index 8becbef0b78..a434d178e4a 100644 --- a/driver/registry_abstract_login.go +++ b/driver/registry_default_login.go @@ -5,18 +5,18 @@ import ( "github.com/ory/kratos/selfservice/flow/login" ) -func (m *RegistryAbstract) LoginHookExecutor() *login.HookExecutor { +func (m *RegistryDefault) LoginHookExecutor() *login.HookExecutor { if m.selfserviceLoginExecutor == nil { - m.selfserviceLoginExecutor = login.NewHookExecutor(m.r, m.c) + m.selfserviceLoginExecutor = login.NewHookExecutor(m, m.c) } return m.selfserviceLoginExecutor } -func (m *RegistryAbstract) PreLoginHooks() []login.PreHookExecutor { +func (m *RegistryDefault) PreLoginHooks() []login.PreHookExecutor { return []login.PreHookExecutor{} } -func (m *RegistryAbstract) PostLoginHooks(credentialsType identity.CredentialsType) []login.PostHookExecutor { +func (m *RegistryDefault) PostLoginHooks(credentialsType identity.CredentialsType) []login.PostHookExecutor { a := m.hooksPost(credentialsType, m.c.SelfServiceLoginAfterHooks(string(credentialsType))) b := make([]login.PostHookExecutor, len(a)) for k, v := range a { @@ -25,17 +25,17 @@ func (m *RegistryAbstract) PostLoginHooks(credentialsType identity.CredentialsTy return b } -func (m *RegistryAbstract) LoginHandler() *login.Handler { +func (m *RegistryDefault) LoginHandler() *login.Handler { if m.selfserviceLoginHandler == nil { - m.selfserviceLoginHandler = login.NewHandler(m.r, m.c) + m.selfserviceLoginHandler = login.NewHandler(m, m.c) } return m.selfserviceLoginHandler } -func (m *RegistryAbstract) LoginRequestErrorHandler() *login.ErrorHandler { +func (m *RegistryDefault) LoginRequestErrorHandler() *login.ErrorHandler { if m.selfserviceLoginRequestErrorHandler == nil { - m.selfserviceLoginRequestErrorHandler = login.NewErrorHandler(m.r, m.c) + m.selfserviceLoginRequestErrorHandler = login.NewErrorHandler(m, m.c) } return m.selfserviceLoginRequestErrorHandler diff --git a/driver/registry_abstract_registration.go b/driver/registry_default_registration.go similarity index 59% rename from driver/registry_abstract_registration.go rename to driver/registry_default_registration.go index 291f5cc6290..87b7730422f 100644 --- a/driver/registry_abstract_registration.go +++ b/driver/registry_default_registration.go @@ -5,7 +5,7 @@ import ( "github.com/ory/kratos/selfservice/flow/registration" ) -func (m *RegistryAbstract) PostRegistrationHooks(credentialsType identity.CredentialsType) []registration.PostHookExecutor { +func (m *RegistryDefault) PostRegistrationHooks(credentialsType identity.CredentialsType) []registration.PostHookExecutor { a := m.hooksPost(credentialsType, m.c.SelfServiceRegistrationAfterHooks(string(credentialsType))) b := make([]registration.PostHookExecutor, len(a)) for k, v := range a { @@ -14,41 +14,41 @@ func (m *RegistryAbstract) PostRegistrationHooks(credentialsType identity.Creden return b } -func (m *RegistryAbstract) PreRegistrationHooks() []registration.PreHookExecutor { +func (m *RegistryDefault) PreRegistrationHooks() []registration.PreHookExecutor { return []registration.PreHookExecutor{} } -func (m *RegistryAbstract) RegistrationExecutor() *registration.HookExecutor { +func (m *RegistryDefault) RegistrationExecutor() *registration.HookExecutor { if m.selfserviceRegistrationExecutor == nil { - m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m.r, m.c) + m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m, m.c) } return m.selfserviceRegistrationExecutor } -func (m *RegistryAbstract) RegistrationHookExecutor() *registration.HookExecutor { +func (m *RegistryDefault) RegistrationHookExecutor() *registration.HookExecutor { if m.selfserviceRegistrationExecutor == nil { - m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m.r, m.c) + m.selfserviceRegistrationExecutor = registration.NewHookExecutor(m, m.c) } return m.selfserviceRegistrationExecutor } -func (m *RegistryAbstract) RegistrationErrorHandler() *registration.ErrorHandler { +func (m *RegistryDefault) RegistrationErrorHandler() *registration.ErrorHandler { if m.seflserviceRegistrationErrorHandler == nil { - m.seflserviceRegistrationErrorHandler = registration.NewErrorHandler(m.r, m.c) + m.seflserviceRegistrationErrorHandler = registration.NewErrorHandler(m, m.c) } return m.seflserviceRegistrationErrorHandler } -func (m *RegistryAbstract) RegistrationHandler() *registration.Handler { +func (m *RegistryDefault) RegistrationHandler() *registration.Handler { if m.selfserviceRegistrationHandler == nil { - m.selfserviceRegistrationHandler = registration.NewHandler(m.r, m.c) + m.selfserviceRegistrationHandler = registration.NewHandler(m, m.c) } return m.selfserviceRegistrationHandler } -func (m *RegistryAbstract) RegistrationRequestErrorHandler() *registration.ErrorHandler { +func (m *RegistryDefault) RegistrationRequestErrorHandler() *registration.ErrorHandler { if m.selfserviceRegistrationRequestErrorHandler == nil { - m.selfserviceRegistrationRequestErrorHandler = registration.NewErrorHandler(m.r, m.c) + m.selfserviceRegistrationRequestErrorHandler = registration.NewErrorHandler(m, m.c) } return m.selfserviceRegistrationRequestErrorHandler diff --git a/driver/registry_memory.go b/driver/registry_memory.go deleted file mode 100644 index ff55151627e..00000000000 --- a/driver/registry_memory.go +++ /dev/null @@ -1,87 +0,0 @@ -package driver - -import ( - "github.com/ory/x/dbal" - - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/profile" - "github.com/ory/kratos/selfservice/flow/registration" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/errorx" - "github.com/ory/kratos/selfservice/persistence" - "github.com/ory/kratos/session" -) - -var _ Registry = new(RegistryMemory) - -func init() { - dbal.RegisterDriver(NewRegistryMemory()) -} - -type RegistryMemory struct { - *RegistryAbstract - - errorManager errorx.Manager - identityPool identity.Pool - sessionManager session.Manager - selfserviceRequestManager persistence.RequestPersister -} - -func NewRegistryMemory() *RegistryMemory { - r := &RegistryMemory{RegistryAbstract: new(RegistryAbstract)} - r.RegistryAbstract.with(r) - return r -} - -func (m *RegistryMemory) Init() error { - return nil -} - -func (m *RegistryMemory) IdentityPool() identity.Pool { - if m.identityPool == nil { - m.identityPool = identity.NewPoolMemory(m.c, m) - } - return m.identityPool -} - -func (m *RegistryMemory) CanHandle(dsn string) bool { - return dsn == "memory" -} - -func (m *RegistryMemory) Ping() error { - return nil -} - -func (m *RegistryMemory) SessionManager() session.Manager { - if m.sessionManager == nil { - m.sessionManager = session.NewManagerMemory(m.c, m) - } - return m.sessionManager -} - -func (m *RegistryMemory) getSelfserviceRequestManager() persistence.RequestPersister { - if m.selfserviceRequestManager == nil { - m.selfserviceRequestManager = persistence.NewRequestManagerMemory() - } - return m.selfserviceRequestManager -} - -func (m *RegistryMemory) RegistrationRequestPersister() registration.RequestPersister { - return m.getSelfserviceRequestManager() -} - -func (m *RegistryMemory) LoginRequestPersister() login.RequestPersister { - return m.getSelfserviceRequestManager() -} - -func (m *RegistryMemory) ProfileRequestPersister() profile.RequestPersister { - return m.getSelfserviceRequestManager() -} - -func (m *RegistryMemory) ErrorManager() errorx.Manager { - if m.errorManager == nil { - m.errorManager = errorx.NewManagerMemory(m, m.c) - } - return m.errorManager -} diff --git a/driver/registry_sql.go b/driver/registry_sql.go deleted file mode 100644 index 1d6d57136f1..00000000000 --- a/driver/registry_sql.go +++ /dev/null @@ -1,205 +0,0 @@ -package driver - -import ( - "fmt" - "os" - "strings" - "testing" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - migrate "github.com/rubenv/sql-migrate" - - "github.com/ory/x/logrusx" - - "github.com/ory/x/dbal" - "github.com/ory/x/sqlcon" - "github.com/ory/x/urlx" - - "github.com/olekukonko/tablewriter" - - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/profile" - - // "github.com/ory/kratos/selfservice/flow/profile" - "github.com/ory/kratos/selfservice/flow/registration" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/errorx" - "github.com/ory/kratos/selfservice/persistence" - "github.com/ory/kratos/session" -) - -var _ Registry = new(RegistrySQL) - -var Migrations = map[string]*dbal.PackrMigrationSource{} - -func init() { - l := logrusx.New() - Migrations[dbal.DriverPostgreSQL] = dbal.NewMustPackerMigrationSource(l, AssetNames(), Asset, []string{"../contrib/sql/migrations/postgres/"}, true) - - dbal.RegisterDriver(NewRegistrySQL()) -} - -type RegistrySQL struct { - *RegistryAbstract - - db *sqlx.DB - errorManager errorx.Manager - identityPool identity.Pool - sessionManager session.Manager - selfServiceRequestPersister persistence.RequestPersister -} - -func NewRegistrySQL() *RegistrySQL { - r := &RegistrySQL{RegistryAbstract: new(RegistryAbstract)} - r.RegistryAbstract.with(r) - return r -} - -func (m *RegistrySQL) WithDB(db *sqlx.DB) Registry { - m.db = db - return m -} - -func (m *RegistrySQL) CanHandle(dsn string) bool { - s := dbal.Canonicalize(urlx.ParseOrFatal(m.l, dsn).Scheme) - return s == dbal.DriverPostgreSQL -} - -func (m *RegistrySQL) Ping() error { - return m.DB().Ping() -} - -func (m *RegistrySQL) IdentityPool() identity.Pool { - if m.identityPool == nil { - m.identityPool = identity.NewPoolSQL(m.c, m, m.DB()) - } - return m.identityPool -} - -func (m *RegistrySQL) ErrorManager() errorx.Manager { - if m.errorManager == nil { - m.errorManager = errorx.NewManagerSQL(m.DB(), m, m.c) - } - return m.errorManager -} - -func (m *RegistrySQL) SessionManager() session.Manager { - if m.sessionManager == nil { - m.sessionManager = session.NewManagerSQL(m.c, m, m.DB()) - } - return m.sessionManager -} - -func (m *RegistrySQL) Init() error { - if m.db != nil { - return nil - } - - var options []sqlcon.OptionModifier - if m.Tracer().IsLoaded() { - options = append(options, sqlcon.WithDistributedTracing(), sqlcon.WithOmitArgsFromTraceSpans()) - } - - connection, err := sqlcon.NewSQLConnection(m.c.DSN(), m.Logger(), options...) - if err != nil { - return err - } - - m.db, err = connection.GetDatabaseRetry(time.Second*5, time.Minute*5) - if err != nil { - return err - } - - return err -} - -func (m *RegistrySQL) DB() *sqlx.DB { - if m.db == nil { - if err := m.Init(); err != nil { - m.Logger().WithError(err).Fatalf("Unable to initialize database.") - } - } - - return m.db -} - -func (m *RegistrySQL) getSelfServiceRequestPersister() persistence.RequestPersister { - if m.selfServiceRequestPersister == nil { - m.selfServiceRequestPersister = persistence.NewRequestManagerMemory() // FIXME - } - return m.selfServiceRequestPersister -} - -func (m *RegistrySQL) ProfileRequestPersister() profile.RequestPersister { - return m.getSelfServiceRequestPersister() -} - -func (m *RegistrySQL) LoginRequestPersister() login.RequestPersister { - return m.getSelfServiceRequestPersister() -} - -func (m *RegistrySQL) RegistrationRequestPersister() registration.RequestPersister { - return m.getSelfServiceRequestPersister() -} - -func (m *RegistrySQL) CreateSchemas(dbName string) (int, error) { - m.Logger().Debugf("Applying %s SQL migrations...", dbName) - - migrate.SetTable("kratos_migration") - total, err := migrate.Exec(m.DB().DB, dbal.Canonicalize(m.DB().DriverName()), Migrations[dbName], migrate.Up) - if err != nil { - return 0, errors.Wrapf(err, "Could not migrate sql schema, applied %d migrations", total) - } - - m.Logger().Debugf("Applied %d %s SQL migrations", total, dbName) - return total, nil -} - -func (m *RegistrySQL) SchemaMigrationPlan(dbName string) (*tablewriter.Table, error) { - table := tablewriter.NewWriter(os.Stdout) - table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) - table.SetCenterSeparator("|") - table.SetAutoMergeCells(true) - table.SetRowLine(true) - table.SetColMinWidth(4, 20) - table.SetHeader([]string{"Driver", "ID", "#", "Query"}) - - migrate.SetTable("kratos_migration") - plans, _, err := migrate.PlanMigration(m.DB().DB, dbal.Canonicalize(m.DB().DriverName()), Migrations[dbName], migrate.Up, 0) - if err != nil { - return nil, err - } - - for _, plan := range plans { - for k, up := range plan.Up { - up = strings.Replace(strings.TrimSpace(up), "\n", "", -1) - up = strings.Join(strings.Fields(up), " ") - if len(up) > 0 { - table.Append([]string{m.db.DriverName(), plan.Id + ".sql", fmt.Sprintf("%d", k), up}) - } - } - } - - return table, nil -} - -func SQLPurgeTestDatabase(t *testing.T, db *sqlx.DB) { - for _, query := range []string{ - "DROP TABLE IF EXISTS kratos_migration", - "DROP TABLE IF EXISTS self_service_request", - "DROP TABLE IF EXISTS identity_credential_identifier", - "DROP TABLE IF EXISTS identity_credential", - "DROP TABLE IF EXISTS session", - "DROP TABLE IF EXISTS identity", - "DROP TYPE IF EXISTS credentials_type", - "DROP TYPE IF EXISTS self_service_request_type", - } { - _, err := db.Exec(query) - if err != nil { - t.Logf("Unable to clean up table %s: %s", query, err) - } - } -} diff --git a/driver/registry_sql_test.go b/driver/registry_sql_test.go deleted file mode 100644 index 7ff8f420d93..00000000000 --- a/driver/registry_sql_test.go +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright © 2015-2018 Aeneas Rekkas - * - * 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. - * - * @author Aeneas Rekkas - * @Copyright 2017-2018 Aeneas Rekkas - * @license Apache-2.0 - */ - -package driver - -import ( - "flag" - "testing" - - "github.com/ory/x/sqlcon/dockertest" -) - -// nolint: staticcheck -func TestMain(m *testing.M) { - flag.Parse() - runner := dockertest.Register() - runner.Exit(m.Run()) -} - -func TestXXMigrations(t *testing.T) { - if testing.Short() { - t.SkipNow() - return - } - - t.Logf("Please implement me") - t.Fail() - - // migratest.RunPackrMigrationTests( - // t, - // migratest.MigrationSchemas{Migrations}, - // migratest.MigrationSchemas{dbal.FindMatchingTestMigrations("../contrib/sql/migrations/tests/", Migrations, AssetNames(), Asset)}, - // SQLPurgeTestDatabase, SQLPurgeTestDatabase, - // func(t *testing.T, dbName string, db *sqlx.DB, _, step, steps int) { - // id := fmt.Sprintf("%d-data", step+1) - // t.Run("poll="+id, func(t *testing.T) { - // t.Run("service=selfservice.NewRequestManagerSQL", func(t *testing.T) { - // m := selfservice.NewRequestManagerSQL(db, requestManagerFactories) - // _, err := m.GetLoginRequest(context.Background(), "1") - // require.NoError(t, err) - // }) - // }) - // }, - // ) -} diff --git a/driver/sql_migration_files.go b/driver/sql_migration_files.go deleted file mode 100644 index 629910c7215..00000000000 --- a/driver/sql_migration_files.go +++ /dev/null @@ -1,352 +0,0 @@ -// Code generated by go-bindata. DO NOT EDIT. -// sources: -// ../contrib/sql/migrations/postgres/1.sql (2.284kB) -// ../contrib/sql/migrations/postgres/2.sql (359B) -// ../contrib/sql/migrations/tests/1_test.sql (758B) -// ../contrib/sql/migrations/tests/2_test.sql (1.1kB) - -package driver - -import ( - "bytes" - "compress/gzip" - "crypto/sha256" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" -) - -func bindataRead(data []byte, name string) ([]byte, error) { - gz, err := gzip.NewReader(bytes.NewBuffer(data)) - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - - var buf bytes.Buffer - _, err = io.Copy(&buf, gz) - clErr := gz.Close() - - if err != nil { - return nil, fmt.Errorf("read %q: %v", name, err) - } - if clErr != nil { - return nil, err - } - - return buf.Bytes(), nil -} - -type asset struct { - bytes []byte - info os.FileInfo - digest [sha256.Size]byte -} - -type bindataFileInfo struct { - name string - size int64 - mode os.FileMode - modTime time.Time -} - -func (fi bindataFileInfo) Name() string { - return fi.name -} -func (fi bindataFileInfo) Size() int64 { - return fi.size -} -func (fi bindataFileInfo) Mode() os.FileMode { - return fi.mode -} -func (fi bindataFileInfo) ModTime() time.Time { - return fi.modTime -} -func (fi bindataFileInfo) IsDir() bool { - return false -} -func (fi bindataFileInfo) Sys() interface{} { - return nil -} - -var _ContribSqlMigrationsPostgres1Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xa4\x56\x4b\x6f\x9b\x4c\x14\xdd\xf3\x2b\xee\x0e\xd0\x97\x48\x9f\x5a\xa5\x8b\x44\xaa\x44\x60\xdc\xa0\x60\xec\xf2\x68\x92\x6e\xd0\x14\xc6\xf6\xd4\x36\xd0\x99\x71\x1e\xaa\xfa\xdf\x2b\x5e\x06\x63\xc0\x38\xf5\x0e\xfb\x9c\x33\x77\xce\x3d\xf7\x9a\xcb\x4b\xf8\x6f\x4b\x97\x0c\x0b\x02\x7e\x2a\xe9\x0e\xd2\x3c\x04\xde\xd3\x1c\x41\xc8\x48\x44\x62\x41\xf1\x86\x07\xe2\x2d\x25\xa0\xb9\x80\x6c\x7f\x0a\x8a\x9c\xd0\x28\x94\x2f\x40\x4e\x31\xe7\x2f\x09\x8b\x64\xf5\xe6\x80\xca\xc9\x66\x11\x70\xc2\x9e\x69\x48\x02\x46\x7e\xed\x08\x17\x6d\x8d\x4d\xb2\xa4\x71\x26\xc2\xc8\x92\x72\xc1\xb0\xa0\x49\x9c\x09\xed\x95\xb4\x5b\x0b\x81\x39\x01\x7b\xe6\x01\x7a\x34\x5d\xcf\x05\x9a\x57\x24\xde\x24\x45\x02\x00\x48\xd7\xd0\xfa\xdc\x9a\x5f\x5c\xe4\x98\x9a\x05\x73\xc7\x9c\x6a\xce\x13\xdc\xa3\xa7\x8b\x1c\x4c\xa3\x36\xf8\x9b\xe6\xe8\x77\x9a\xa3\x7c\xb8\xba\x52\xf3\x53\x6c\xdf\xb2\xc0\xb7\xcd\xaf\x3e\x2a\x38\x82\x61\x2a\x78\x93\xf3\x93\x27\xf1\x8f\xea\x61\xcf\x31\xd0\x44\xf3\x2d\x0f\xe4\xdf\x7f\xe4\xeb\xeb\x1c\xd3\x14\x08\x78\xb8\x22\x5b\x1c\xec\xd8\x06\x04\x79\x15\xd0\x16\x90\x46\xde\x3b\xa8\x9b\xd2\x61\xc1\xe0\xe5\x4b\x81\x74\x9d\xc1\x4c\xdb\x03\x07\x4d\x90\x83\x6c\x1d\xd5\xf2\xa0\xa4\x6b\x15\x66\x36\x18\xc8\x42\x1e\x02\x5d\x73\x75\xcd\x28\xbd\xd8\x12\xb1\x4a\x4a\x0f\x8f\xb2\x51\x5d\xa4\x80\x86\x49\xbc\xa0\xcb\x0e\xc3\x4e\x99\xf6\x0e\x1f\x82\xe2\xbb\x05\x25\xac\x37\x15\xa3\xdd\x69\xc8\x0e\x1a\xd5\x00\x0e\x7a\x56\xd7\x76\x66\xf0\xf4\x3b\xa4\xdf\x83\xb2\x21\xf1\x52\xac\x94\x5a\x46\x85\xcf\xf0\xbf\x7a\xca\xa6\xae\xf9\xeb\x31\xe7\x8c\x81\xa9\xaa\xfe\xf8\x49\x6d\x59\xdb\x79\x05\xf2\x9a\x52\x46\x78\x80\xcb\xb8\x7b\xe6\x14\xb9\x9e\x36\x9d\xc3\x83\xe9\xdd\xe5\x8f\xf0\x7d\x66\x23\x68\x85\x87\x72\xbe\x23\xd1\x9e\x36\x82\xb7\x4f\x92\x3d\x7b\x50\xd4\x42\xa5\xda\x3a\xd9\xc8\xe5\x83\xd8\x1c\xbb\xce\xe2\x0f\x79\x2b\x82\x23\xc2\xf8\x71\x7c\x47\xe5\xb8\xd0\xc2\xa1\xa0\xcf\xa4\x66\x1c\x4d\x4d\xad\xb5\x3f\xbf\x98\xb2\x7a\xe5\xfc\xcb\xf9\x6b\x1a\x1f\xb4\xb0\x7f\x2d\x8f\xdd\x43\x9c\x70\x4e\x93\xb8\x6f\xd0\x06\xc2\xc4\x47\xa7\x69\x54\x98\xfa\x53\x31\x1c\xa6\xd3\xbc\xae\x30\xe1\x9d\x58\x65\x8d\x0b\xb1\x28\xc4\xde\xa5\xd2\xdc\xc1\x95\x5d\xe7\x2d\xe2\x66\x7b\x0a\x77\xc0\xb4\x0d\xf4\x08\x31\xde\x92\x0c\xde\xd5\x61\x50\x68\x74\x91\x67\x21\x63\x37\xff\xf0\x8d\xe4\x25\x96\x0c\x67\x36\x2f\x7b\x3d\xbc\x5d\x6f\x4e\x40\x0f\x7e\x2f\x83\xd2\xc9\x69\x01\x8f\x2b\xae\x00\xd9\xab\x84\x39\xa9\xb2\xd7\x9e\x9e\x6e\x54\x6f\xc6\x6f\xa4\xbf\x01\x00\x00\xff\xff\x21\x00\xe7\x1b\xec\x08\x00\x00") - -func ContribSqlMigrationsPostgres1SqlBytes() ([]byte, error) { - return bindataRead( - _ContribSqlMigrationsPostgres1Sql, - "../contrib/sql/migrations/postgres/1.sql", - ) -} - -func ContribSqlMigrationsPostgres1Sql() (*asset, error) { - bytes, err := ContribSqlMigrationsPostgres1SqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "../contrib/sql/migrations/postgres/1.sql", size: 2284, mode: os.FileMode(0644), modTime: time.Unix(1571236390, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xa9, 0xf1, 0xff, 0x23, 0x86, 0xfd, 0x23, 0x68, 0xe8, 0xe3, 0xdc, 0x67, 0xfd, 0x17, 0x6b, 0x3a, 0xcb, 0x40, 0xee, 0x1a, 0x31, 0x7d, 0x8a, 0xe9, 0x86, 0x61, 0xca, 0xf4, 0x56, 0xd0, 0xb8, 0x48}} - return a, nil -} - -var _ContribSqlMigrationsPostgres2Sql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x90\xc1\x4e\xc3\x30\x10\x44\xef\xfe\x8a\x39\x82\xa0\x5f\xd0\x93\x43\xb6\xb0\xc2\x89\x83\x63\x0b\xca\x25\x0a\xb0\x45\x81\x92\x54\x76\x45\x7f\x1f\x35\xe9\xa1\x12\x55\xe6\xb6\xd2\x1b\xcd\xea\x2d\x16\xb8\xf9\xe9\x3e\x63\xbb\x17\x84\x9d\xba\x73\xa4\x3d\xc1\xeb\xcc\x10\x78\x85\xd2\x7a\xd0\x0b\xd7\xbe\x46\x92\xed\xa6\x49\x12\x7f\xbb\x77\x69\x24\xc6\x21\xaa\x2b\x05\x00\xbb\x6f\x4c\xc9\xf8\xbe\x26\xc7\xda\xa0\x72\x5c\x68\xb7\xc6\x23\xad\x6f\x47\xa6\xfb\x38\x31\x21\x70\x8e\x4b\x39\x2e\x95\xc1\x18\x84\x92\x9f\x02\x4d\xb5\x71\x26\x01\xf8\x4a\x43\xff\x36\x57\x9b\xf8\x24\xd2\x37\xed\x1e\xf0\x5c\x50\xed\x75\x51\xe1\x99\xfd\xc3\x78\xe2\xd5\x96\x74\xc6\x1e\xda\xd4\x1c\x79\x64\xd6\x9a\xf9\x97\x72\x5a\xe9\x60\x3c\x36\xed\x36\x89\xba\x5e\x2a\x75\xae\x2d\x1f\x0e\xbd\xca\x9d\xad\x4e\xda\xfe\x8b\x5a\xaa\xbf\x00\x00\x00\xff\xff\x18\xf7\xfe\xfd\x67\x01\x00\x00") - -func ContribSqlMigrationsPostgres2SqlBytes() ([]byte, error) { - return bindataRead( - _ContribSqlMigrationsPostgres2Sql, - "../contrib/sql/migrations/postgres/2.sql", - ) -} - -func ContribSqlMigrationsPostgres2Sql() (*asset, error) { - bytes, err := ContribSqlMigrationsPostgres2SqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "../contrib/sql/migrations/postgres/2.sql", size: 359, mode: os.FileMode(0644), modTime: time.Unix(1572877373, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0xcb, 0x12, 0x26, 0xb8, 0xf9, 0xce, 0x77, 0xb4, 0x76, 0x0, 0xae, 0xac, 0xce, 0xac, 0x85, 0x94, 0x4a, 0x6e, 0x38, 0x6f, 0x64, 0x1f, 0x3a, 0x38, 0x97, 0x58, 0x9f, 0x26, 0x54, 0x7e, 0xb3, 0xe}} - return a, nil -} - -var _ContribSqlMigrationsTests1_testSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x52\xc1\x4e\xc2\x40\x10\xbd\xf7\x2b\x26\xbd\xb4\xc4\x05\xc2\x15\x2f\x9a\xc8\x81\xc4\x40\x22\xa0\xc7\x66\xdd\x9d\xb6\x13\xda\x6e\xdd\xd9\x52\x88\xe1\xdf\x4d\x4b\x95\xa2\xc4\x78\xf2\x34\xd3\xd7\xc9\x7b\x33\xfb\xde\x70\x08\x37\x39\x25\x56\x3a\x84\x4d\xe9\xcd\x17\xab\xd9\xd3\x1a\xe6\x8b\xf5\x12\x48\x63\xe1\xc8\x1d\x20\x24\x2d\xc0\x59\x49\x8e\x23\x56\x29\xe6\x32\xaa\x6c\x36\xf0\x9e\xef\x1f\x37\xb3\x15\x84\x81\x96\x4e\x0e\x27\x81\x80\x20\x36\x26\x18\xdc\x7a\x57\x79\x22\x65\xb1\x6d\x65\xd6\x50\x76\x60\xb9\x15\x90\xa3\x4b\x8d\x16\xa0\x4c\x11\x53\x72\x26\x9e\x08\x08\x4a\xc9\x5c\x1b\xab\x1b\xf6\x77\x3f\x36\xc6\x9f\xfa\xaf\xd2\xfa\xc7\xbf\xe8\x44\x27\x2c\x26\xb4\x3d\xc9\xde\xff\x46\xfd\x3c\x73\xa9\x7c\xba\xea\x0e\xf7\x32\x2f\x33\x1c\x19\x9b\xfc\xb7\xa4\x32\xf9\x0f\x49\xc6\x2c\x8e\x18\xed\x8e\x14\x46\x16\xdf\x2a\x64\x77\x72\x08\xf7\x25\x59\xe4\x48\x3a\x01\xc4\x5c\xa1\x6e\xdb\x6e\xa6\xb1\xec\xfc\x91\xa2\xd4\x68\x59\x80\x54\x8e\x76\xf8\x69\x01\x0b\xd8\x52\xa1\x2f\x96\x5a\x2c\x5f\xc2\xc1\x57\x09\x52\xe7\x4a\x9e\x8e\xc7\x75\x5d\x8f\x8c\x3d\x8c\x38\x1d\xb7\xd6\x1c\x83\xef\x66\xb5\x48\x66\x12\x2a\xae\x1c\xc1\x4c\xa6\x80\x90\x7f\x59\x5c\x56\x2e\x6d\x9e\x49\x49\xd7\x21\xbd\xd0\xf4\xd2\xc7\xa4\xdb\xf0\x5d\x2e\xda\x95\x49\x23\xdd\x0f\xf9\x83\xa9\x0b\xef\x23\x00\x00\xff\xff\x3c\xee\x8d\x2f\xf6\x02\x00\x00") - -func ContribSqlMigrationsTests1_testSqlBytes() ([]byte, error) { - return bindataRead( - _ContribSqlMigrationsTests1_testSql, - "../contrib/sql/migrations/tests/1_test.sql", - ) -} - -func ContribSqlMigrationsTests1_testSql() (*asset, error) { - bytes, err := ContribSqlMigrationsTests1_testSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "../contrib/sql/migrations/tests/1_test.sql", size: 758, mode: os.FileMode(0644), modTime: time.Unix(1565964771, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x19, 0xa2, 0xa9, 0x4b, 0x9f, 0x7a, 0x6b, 0x26, 0x64, 0xcb, 0x9d, 0xe2, 0x41, 0xea, 0x5c, 0xb9, 0x98, 0xb4, 0xdc, 0xed, 0x87, 0x4e, 0xfe, 0x4f, 0xcc, 0x5b, 0xed, 0xe5, 0x3f, 0xd8, 0xff, 0x42}} - return a, nil -} - -var _ContribSqlMigrationsTests2_testSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x54\xc1\x6a\xdb\x40\x10\xbd\xeb\x2b\x06\x5d\x64\xd3\x5d\x3b\x51\x4a\x88\xdd\x4b\x0b\xcd\x21\x60\x1c\x68\xe2\xf6\x50\x8a\x18\x69\x47\xd6\x12\x49\xab\xee\xac\xa2\x84\xe0\x7f\x2f\x5a\xab\xb5\xdc\x86\xd0\x43\xc8\x69\x67\x47\xcb\xbc\x37\xef\x3d\x24\x25\xbc\xab\xf4\xd6\xa2\x23\xd8\x34\xc1\xd5\xfa\xe6\xf2\xcb\x2d\x5c\xad\x6f\xaf\x41\x2b\xaa\x9d\x76\x8f\x30\xd1\x4a\x80\xb3\xa8\x1d\x27\x9c\x15\x54\x61\xd2\xda\x72\x1a\x7c\xfd\xb4\xda\x5c\xde\xc0\x24\x52\xe8\x50\xc6\x91\x80\x28\x37\x26\x9a\x7e\x08\x9e\x9d\x93\x64\x96\x7c\x89\x65\x3f\x72\x68\x36\x77\x02\x2a\x72\x85\x51\x02\x32\x53\xe7\x7a\x7b\x18\x1c\x0b\x88\x1a\x64\xee\x8c\x55\xfd\xf4\xa7\x30\x37\x26\x5c\x86\x29\xda\x70\xf7\x3f\x38\xc9\xbe\x97\x6b\xb2\x23\xc8\xd1\xf7\x1e\xfd\xf0\xe6\x18\x79\xbf\xd5\x47\x7a\xc0\xaa\x29\x69\x66\xec\xf6\xad\x21\x33\x53\xfd\x03\xc9\x54\xe6\x09\x93\xbd\xd7\x19\x25\x96\x7e\xb6\xc4\x6e\xef\x10\x3d\x34\xda\x12\x27\xe8\x04\x68\xe6\x96\x94\x2f\x87\x37\xbd\x65\x87\x4b\x41\xa8\xc8\xb2\x00\xcc\x9c\xbe\xa7\xdf\x16\xb0\x80\x3b\x5d\xab\x23\x52\xeb\xeb\x6f\x93\xe9\x9f\x23\x2a\x9c\x6b\x78\x39\x9f\x77\x5d\x37\x33\xf6\x71\xc6\xc5\xdc\x5b\xb3\x8b\xfe\x36\xcb\x77\x4a\xb3\xd5\xf5\x33\x4b\x30\x6b\x53\xc3\x84\x5f\x20\x8e\xad\x2b\x7a\x99\x32\x74\x43\x67\x14\x9a\x51\xfa\x58\x2b\x1f\xbe\x63\xa2\xc3\x11\xbf\xa8\x1f\x59\x6b\xec\xa0\x5e\x5f\xb2\x00\x26\xaa\x3d\x58\x87\x9c\xf4\x97\x11\x52\x1c\xc7\xb1\x4c\xb3\xc5\x42\x2e\xb2\x93\x54\xbe\xa7\xfc\x42\xa6\xe9\xb9\x92\xe7\x69\xba\x90\xa9\x3a\xbb\x90\x27\x78\x7a\xda\xef\xfd\x3d\x00\xf0\x69\x15\x01\xc0\x53\x00\x00\x10\xd6\x58\x51\xb8\x04\x1f\xdf\x00\x60\x17\xfc\xe8\x59\x6f\x56\x2b\x01\x39\x96\x4c\xaf\x4c\xf5\xec\xd5\xa9\xee\x25\x75\xb6\xf5\x54\xc7\xbf\x8e\xcf\xa6\xab\x83\x5f\x01\x00\x00\xff\xff\x6a\x67\x4d\x4a\x4c\x04\x00\x00") - -func ContribSqlMigrationsTests2_testSqlBytes() ([]byte, error) { - return bindataRead( - _ContribSqlMigrationsTests2_testSql, - "../contrib/sql/migrations/tests/2_test.sql", - ) -} - -func ContribSqlMigrationsTests2_testSql() (*asset, error) { - bytes, err := ContribSqlMigrationsTests2_testSqlBytes() - if err != nil { - return nil, err - } - - info := bindataFileInfo{name: "../contrib/sql/migrations/tests/2_test.sql", size: 1100, mode: os.FileMode(0644), modTime: time.Unix(1572885497, 0)} - a := &asset{bytes: bytes, info: info, digest: [32]uint8{0x9b, 0x50, 0x6e, 0xbc, 0x65, 0xd4, 0x83, 0x11, 0x92, 0x8d, 0x10, 0xa1, 0xa0, 0x0, 0x5d, 0x60, 0x56, 0x4a, 0x81, 0xc9, 0x1f, 0x36, 0x48, 0xe3, 0xc4, 0x54, 0x6e, 0xce, 0x16, 0x4f, 0xb8, 0x1f}} - return a, nil -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// AssetString returns the asset contents as a string (instead of a []byte). -func AssetString(name string) (string, error) { - data, err := Asset(name) - return string(data), err -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// MustAssetString is like AssetString but panics when Asset would return an -// error. It simplifies safe initialization of global variables. -func MustAssetString(name string) string { - return string(MustAsset(name)) -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetDigest returns the digest of the file with the given name. It returns an -// error if the asset could not be found or the digest could not be loaded. -func AssetDigest(name string) ([sha256.Size]byte, error) { - canonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[canonicalName]; ok { - a, err := f() - if err != nil { - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s can't read by error: %v", name, err) - } - return a.digest, nil - } - return [sha256.Size]byte{}, fmt.Errorf("AssetDigest %s not found", name) -} - -// Digests returns a map of all known files and their checksums. -func Digests() (map[string][sha256.Size]byte, error) { - mp := make(map[string][sha256.Size]byte, len(_bindata)) - for name := range _bindata { - a, err := _bindata[name]() - if err != nil { - return nil, err - } - mp[name] = a.digest - } - return mp, nil -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "../contrib/sql/migrations/postgres/1.sql": ContribSqlMigrationsPostgres1Sql, - "../contrib/sql/migrations/postgres/2.sql": ContribSqlMigrationsPostgres2Sql, - "../contrib/sql/migrations/tests/1_test.sql": ContribSqlMigrationsTests1_testSql, - "../contrib/sql/migrations/tests/2_test.sql": ContribSqlMigrationsTests2_testSql, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"}, -// AssetDir("data/img") would return []string{"a.png", "b.png"}, -// AssetDir("foo.txt") and AssetDir("notexist") would return an error, and -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - canonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(canonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} - -var _bintree = &bintree{nil, map[string]*bintree{ - "..": &bintree{nil, map[string]*bintree{ - "contrib": &bintree{nil, map[string]*bintree{ - "sql": &bintree{nil, map[string]*bintree{ - "migrations": &bintree{nil, map[string]*bintree{ - "postgres": &bintree{nil, map[string]*bintree{ - "1.sql": &bintree{ContribSqlMigrationsPostgres1Sql, map[string]*bintree{}}, - "2.sql": &bintree{ContribSqlMigrationsPostgres2Sql, map[string]*bintree{}}, - }}, - "tests": &bintree{nil, map[string]*bintree{ - "1_test.sql": &bintree{ContribSqlMigrationsTests1_testSql, map[string]*bintree{}}, - "2_test.sql": &bintree{ContribSqlMigrationsTests2_testSql, map[string]*bintree{}}, - }}, - }}, - }}, - }}, - }}, -}} - -// RestoreAsset restores an asset under the given directory. -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) -} - -// RestoreAssets restores an asset under the given directory recursively. -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - canonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(canonicalName, "/")...)...) -} diff --git a/go.mod b/go.mod index 8d943dc5708..c3be82d2567 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ replace github.com/santhosh-tekuri/jsonschema/v2 => github.com/ory/jsonschema/v2 require ( github.com/bxcodec/faker v2.0.1+incompatible + github.com/cenkalti/backoff v2.1.1+incompatible github.com/coreos/go-oidc v2.0.0+incompatible github.com/fsnotify/fsnotify v1.4.7 github.com/go-errors/errors v1.0.1 @@ -20,7 +21,13 @@ require ( github.com/go-swagger/go-swagger v0.19.0 github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 // indirect github.com/gobuffalo/httptest v1.0.2 - github.com/gobuffalo/packr/v2 v2.0.0-rc.15 + github.com/gobuffalo/nulls v0.1.0 // indirect + github.com/gobuffalo/packr v1.22.0 + github.com/gobuffalo/packr/v2 v2.7.1 + github.com/gobuffalo/pop v4.12.2+incompatible + github.com/gobuffalo/tags v2.1.7+incompatible // indirect + github.com/gobuffalo/uuid v2.0.5+incompatible + github.com/gofrs/uuid v3.2.0+incompatible github.com/golang/gddo v0.0.0-20190904175337-72a348e765d2 github.com/golang/mock v1.3.1 github.com/google/go-github/v27 v27.0.1 @@ -31,30 +38,26 @@ require ( github.com/gorilla/sessions v1.1.3 github.com/imdario/mergo v0.3.7 github.com/jessevdk/go-flags v1.4.0 // indirect - github.com/jmoiron/sqlx v1.2.0 github.com/jteeuwen/go-bindata v3.0.7+incompatible github.com/julienschmidt/httprouter v1.2.0 github.com/justinas/nosurf v0.0.0-20190118163749-6453469bdcc9 github.com/leodido/go-urn v1.1.0 // indirect - github.com/lib/pq v1.2.0 // indirect github.com/luna-duclos/instrumentedsql v1.1.1 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect + github.com/mattn/go-sqlite3 v1.13.0 // indirect github.com/mattn/goveralls v0.0.4 github.com/mitchellh/go-homedir v1.1.0 - github.com/olekukonko/tablewriter v0.0.1 github.com/ory/dockertest v3.3.5+incompatible github.com/ory/go-acc v0.0.0-20181118080137-ddc355013f90 - github.com/ory/go-convenience v0.1.0 github.com/ory/gojsonschema v1.2.0 github.com/ory/graceful v0.1.1 - github.com/ory/herodot v0.6.2 + github.com/ory/herodot v0.6.3 github.com/ory/viper v1.5.6 - github.com/ory/x v0.0.83 + github.com/ory/x v0.0.84 github.com/pelletier/go-toml v1.6.0 // indirect github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 github.com/pkg/errors v0.8.1 github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect - github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7 + github.com/rogpeppe/go-internal v1.5.0 // indirect github.com/santhosh-tekuri/jsonschema/v2 v2.1.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 @@ -67,12 +70,13 @@ require ( github.com/toqueteos/webbrowser v1.1.0 // indirect github.com/urfave/negroni v1.0.0 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 - golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 // indirect + golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c + golang.org/x/net v0.0.0-20191125084936-ffdde1057850 // indirect golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect google.golang.org/appengine v1.6.5 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v9 v9.28.0 - gopkg.in/yaml.v2 v2.2.5 // indirect ) diff --git a/go.sum b/go.sum index ca5c3df5b17..e4f887a1fd9 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/aws/aws-sdk-go v1.23.19/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-xray-sdk-go v0.9.4/go.mod h1:XtMKdBQfpVut+tJEwI7+dJFRxxRdxHDyVNp2tHXRq04= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= @@ -45,8 +47,12 @@ github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QH github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cihub/seelog v0.0.0-20170130134532-f561c5e57575/go.mod h1:9d6lWj8KzO/fd/NrVaLscBKmPigpZpn5YawRPw+e3Yo= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0= github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= +github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051 h1:eApuUG8W2EtBVwxqLlY2wgoqDYOg3WvIHGvW4fUbbow= +github.com/cockroachdb/cockroach-go v0.0.0-20190925194419-606b3d062051/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/negroni v1.0.0/go.mod h1:v0y3T5G7Y1UlFfyxFn/QLRU4a2EuNau2iZY63YTKWo0= @@ -59,8 +65,10 @@ github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5 github.com/coreos/go-oidc v2.0.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -73,6 +81,7 @@ github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20181003060214-f58a169a71a5/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= @@ -137,6 +146,7 @@ github.com/go-swagger/go-swagger v0.19.0 h1:w/tXke7vqKHgY8slisWOnSDuhQXujt4Qag2j github.com/go-swagger/go-swagger v0.19.0/go.mod h1:fOcXeMI1KPNv3uk4u7cR4VSyq0NyrYx4SS1/ajuTWDg= github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0= github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0= +github.com/gobuffalo/attrs v0.1.0/go.mod h1:fmNpaWyHM0tRm8gCZWKx8yY9fvaNLo2PyzBNSrBZ5Hw= github.com/gobuffalo/buffalo v0.12.8-0.20181004233540-fac9bb505aa8/go.mod h1:sLyT7/dceRXJUxSsE813JTQtA3Eb1vjxWfo/N//vXIY= github.com/gobuffalo/buffalo v0.13.0/go.mod h1:Mjn1Ba9wpIbpbrD+lIDMy99pQ0H0LiddMIIDGse7qT4= github.com/gobuffalo/buffalo-plugins v1.0.2/go.mod h1:pOp/uF7X3IShFHyobahTkTLZaeUXwb0GrUTb9ngJWTs= @@ -166,6 +176,11 @@ github.com/gobuffalo/envy v1.6.11/go.mod h1:Fiq52W7nrHGDggFPhn2ZCcHw4u/rqXkqo+i7 github.com/gobuffalo/envy v1.6.12/go.mod h1:qJNrJhKkZpEW0glh5xP2syQHH5kgdmgsKss2Kk8PTP0= github.com/gobuffalo/envy v1.6.15 h1:OsV5vOpHYUpP7ZLS6sem1y40/lNX1BZj+ynMiRi21lQ= github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= +github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8= +github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= +github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs= +github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w= github.com/gobuffalo/events v1.0.3/go.mod h1:Txo8WmqScapa7zimEQIwgiJBvMECMe9gJjsKNPN3uZw= github.com/gobuffalo/events v1.0.7/go.mod h1:z8txf6H9jWhQ5Scr7YPLWg/cgXBRj8Q4uYI+rsVCCSQ= github.com/gobuffalo/events v1.0.8/go.mod h1:A5KyqT1sA+3GJiBE4QKZibse9mtOcI9nw8gGrDdqYGs= @@ -176,7 +191,10 @@ github.com/gobuffalo/events v1.1.7/go.mod h1:6fGqxH2ing5XMb3EYRq9LEkVlyPGs4oO/eL github.com/gobuffalo/events v1.1.8/go.mod h1:UFy+W6X6VbCWS8k2iT81HYX65dMtiuVycMy04cplt/8= github.com/gobuffalo/events v1.1.9 h1:ukq5ys/h0TuiX7eLJyZBD1dJOy0r19JTEYmgXKG9j+Y= github.com/gobuffalo/events v1.1.9/go.mod h1:/0nf8lMtP5TkgNbzYxR6Bl4GzBy5s5TebgNTdRfRbPM= +github.com/gobuffalo/fizz v1.0.12 h1:JJOkmlStog5AiBL434UoGMJ896p3MnTnzedFVaZSF3k= github.com/gobuffalo/fizz v1.0.12/go.mod h1:C0sltPxpYK8Ftvf64kbsQa2yiCZY4RZviurNxXdAKwc= +github.com/gobuffalo/fizz v1.9.5 h1:Qh0GkP7MYtJs9RZwBkPJ0CzEXynVowdNfrjg8b+TOxA= +github.com/gobuffalo/fizz v1.9.5/go.mod h1:v9cFl56oXm+hNNayTsIQHnq209bTDUbIM8GYWCJw3TE= github.com/gobuffalo/flect v0.0.0-20180907193754-dc14d8acaf9f/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= github.com/gobuffalo/flect v0.0.0-20181002182613-4571df4b1daf/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= github.com/gobuffalo/flect v0.0.0-20181007231023-ae7ed6bfe683/go.mod h1:rCiQgmAE4axgBNl3jZWzS5rETRYTGOsrixTRaCPzNdA= @@ -189,6 +207,11 @@ github.com/gobuffalo/flect v0.0.0-20181210151238-24a2b68e0316/go.mod h1:en58vff7 github.com/gobuffalo/flect v0.0.0-20190104192022-4af577e09bf2/go.mod h1:en58vff74S9b99Eg42Dr+/9yPu437QjlNsO/hBYPuOk= github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794 h1:HZOs07hF3AmoaUj4HJQHV5RqfOuGnPZI7aFcireIrww= github.com/gobuffalo/flect v0.0.0-20190117212819-a62e61d96794/go.mod h1:397QT6v05LkZkn07oJXXT6y9FCfwC8Pug0WA2/2mE9k= +github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobuffalo/flect v0.1.6 h1:D7KWNRFiCknJKA495/e1BO7oxqf8tbieaLv/ehoZ/+g= +github.com/gobuffalo/flect v0.1.6/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= +github.com/gobuffalo/flect v0.1.7 h1:qQqM2eGdM6tJX8yHKYBM0wVHBLjUT7Qs6uk5jnAhOwI= +github.com/gobuffalo/flect v0.1.7/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/genny v0.0.0-20180924032338-7af3a40f2252/go.mod h1:tUTQOogrr7tAQnhajMSH6rv1BVev34H2sa1xNHMy94g= github.com/gobuffalo/genny v0.0.0-20181003150629-3786a0744c5d/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= github.com/gobuffalo/genny v0.0.0-20181005145118-318a41a134cc/go.mod h1:WAd8HmjMVrnkAZbmfgH5dLBUchsZfqzp/WS5sQz+uTM= @@ -213,9 +236,23 @@ github.com/gobuffalo/genny v0.0.0-20181211165820-e26c8466f14d/go.mod h1:sHnK+ZSU github.com/gobuffalo/genny v0.0.0-20190104222617-a71664fc38e7/go.mod h1:QPsQ1FnhEsiU8f+O0qKWXz2RE4TiDqLVChWkBuh1WaY= github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5 h1:boQS3dA9PxhyufJEWIILrG6pJQbDnpwP2rFyvWacdoY= github.com/gobuffalo/genny v0.0.0-20190112155932-f31a84fcacf5/go.mod h1:CIaHCrSIuJ4il6ka3Hub4DR4adDrGoXGEEt2FbBxoIo= +github.com/gobuffalo/genny v0.2.0 h1:4srOvuxvdxKsc6izHdHkS8KpQCtUHp6QZm5EUc5vksU= +github.com/gobuffalo/genny v0.2.0/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk= +github.com/gobuffalo/genny v0.3.0/go.mod h1:ywJ2CoXrTZj7rbS8HTbzv7uybnLKlsNSBhEQ+yFI3E8= +github.com/gobuffalo/genny v0.4.1 h1:ylgRyFoVGtfq92Ziq0kyi0Sdwh//pqWEwg+vD3eK1ZA= +github.com/gobuffalo/genny v0.4.1/go.mod h1:dpded+KBgICFciAb+6R5Lo+1VxzofjqHgKqFYIL8M7U= github.com/gobuffalo/github_flavored_markdown v1.0.4/go.mod h1:uRowCdK+q8d/RF0Kt3/DSalaIXbb0De/dmTqMQdkQ4I= github.com/gobuffalo/github_flavored_markdown v1.0.5/go.mod h1:U0643QShPF+OF2tJvYNiYDLDGDuQmJZXsf/bHOJPsMY= +github.com/gobuffalo/github_flavored_markdown v1.0.7 h1:Vjvz4wqOnviiLEfTh5bh270b3lhpJiwwQEWOWmHMwY8= github.com/gobuffalo/github_flavored_markdown v1.0.7/go.mod h1:w93Pd9Lz6LvyQXEG6DktTPHkOtCbr+arAD5mkwMzXLI= +github.com/gobuffalo/github_flavored_markdown v1.1.0 h1:8Zzj4fTRl/OP2R7sGerzSf6g2nEJnaBEJe7UAOiEvbQ= +github.com/gobuffalo/github_flavored_markdown v1.1.0/go.mod h1:TSpTKWcRTI0+v7W3x8dkSKMLJSUpuVitlptCkpeY8ic= +github.com/gobuffalo/gogen v0.2.0/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360= +github.com/gobuffalo/helpers v0.2.2 h1:AAu+rUINhzIii77xzuih26Q0ZJgOcLkf37FaBcv2diU= +github.com/gobuffalo/helpers v0.2.2/go.mod h1:xYbzUdCUpVzLwLnqV8HIjT6hmG0Cs7YIBCJkNM597jw= +github.com/gobuffalo/helpers v0.2.4/go.mod h1:NX7v27yxPDOPTgUFYmJ5ow37EbxdoLraucOGvMNawyk= +github.com/gobuffalo/helpers v0.4.0 h1:DR/iYihrVCXv1cYeIGSK3EZz2CljO+DqDLQPWZAod9c= +github.com/gobuffalo/helpers v0.4.0/go.mod h1:2q/ZnVxCehM4/y1bNz3+wXsvWvWUY+iTUr7mPC6QqGQ= github.com/gobuffalo/httptest v1.0.2 h1:LWp2khlgA697h4BIYWW2aRxvB93jMnBrbakQ/r2KLzs= github.com/gobuffalo/httptest v1.0.2/go.mod h1:7T1IbSrg60ankme0aDLVnEY0h056g9M1/ZvpVThtB7E= github.com/gobuffalo/licenser v0.0.0-20180924033006-eae28e638a42/go.mod h1:Ubo90Np8gpsSZqNScZZkVXXAo5DGhTb+WYFIjlnog8w= @@ -232,10 +269,19 @@ github.com/gobuffalo/logger v0.0.0-20181109185836-3feeab578c17/go.mod h1:oNErH0x github.com/gobuffalo/logger v0.0.0-20181117211126-8e9b89b7c264/go.mod h1:5etB91IE0uBlw9k756fVKZJdS+7M7ejVhmpXXiSFj0I= github.com/gobuffalo/logger v0.0.0-20181127160119-5b956e21995c h1:Z/ppYX6EtPEysbW4VEGz2dO+4F4VTthWp2sWRUCANdU= github.com/gobuffalo/logger v0.0.0-20181127160119-5b956e21995c/go.mod h1:+HxKANrR9VGw9yN3aOAppJKvhO05ctDi63w4mDnKv2U= +github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8= +github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg= +github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= +github.com/gobuffalo/logger v1.0.3 h1:YaXOTHNPCvkqqA7w05A4v0k2tCdpr+sgFlgINbQ6gqc= +github.com/gobuffalo/logger v1.0.3/go.mod h1:SoeejUwldiS7ZsyCBphOGURmWdwUFXs0J7TCjEhjKxM= +github.com/gobuffalo/makr v1.1.5 h1:lOlpv2iz0dNa4qse0ZYQgbtT+ybwVxWEAcOZbcPmeYc= github.com/gobuffalo/makr v1.1.5/go.mod h1:Y+o0btAH1kYAMDJW/TX3+oAXEu0bmSLLoC9mIFxtzOw= github.com/gobuffalo/mapi v1.0.0/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= github.com/gobuffalo/mapi v1.0.1 h1:JRuTiZzDEZhBHkFiHTxJkYRT6CbYuL0K/rn+1byJoEA= github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc= +github.com/gobuffalo/mapi v1.1.0/go.mod h1:pqQ1XAqvpy/JYtRwoieNps2yU8MFiMxBUpAm2FBtQ50= github.com/gobuffalo/meta v0.0.0-20181018155829-df62557efcd3/go.mod h1:XTTOhwMNryif3x9LkTTBO/Llrveezd71u3quLd0u7CM= github.com/gobuffalo/meta v0.0.0-20181018192820-8c6cef77dab3/go.mod h1:E94EPzx9NERGCY69UWlcj6Hipf2uK/vnfrF4QD0plVE= github.com/gobuffalo/meta v0.0.0-20181025145500-3a985a084b0a/go.mod h1:YDAKBud2FP7NZdruCSlmTmDOZbVSa6bpK7LJ/A/nlKg= @@ -250,6 +296,8 @@ github.com/gobuffalo/mw-forcessl v0.0.0-20180802152810-73921ae7a130/go.mod h1:Jv github.com/gobuffalo/mw-i18n v0.0.0-20180802152014-e3060b7e13d6/go.mod h1:91AQfukc52A6hdfIfkxzyr+kpVYDodgAeT5cjX1UIj4= github.com/gobuffalo/mw-paramlogger v0.0.0-20181005191442-d6ee392ec72e/go.mod h1:6OJr6VwSzgJMqWMj7TYmRUqzNe2LXu/W1rRW4MAz/ME= github.com/gobuffalo/mw-tokenauth v0.0.0-20181001105134-8545f626c189/go.mod h1:UqBF00IfKvd39ni5+yI5MLMjAf4gX7cDKN/26zDOD6c= +github.com/gobuffalo/nulls v0.1.0 h1:pR3SDzXyFcQrzyPreZj+OzNHSxI4DphSOFaQuidxrfw= +github.com/gobuffalo/nulls v0.1.0/go.mod h1:/HRtuDRoVoN5fABk3J6jzZaGEdcIZEMs0qczj71eKZY= github.com/gobuffalo/packd v0.0.0-20181027182251-01ad393492c8/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= github.com/gobuffalo/packd v0.0.0-20181027190505-aafc0d02c411/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= github.com/gobuffalo/packd v0.0.0-20181027194105-7ae579e6d213/go.mod h1:SmdBdhj6uhOsg1Ui4SFAyrhuc7U4VCildosO5IDJ3lc= @@ -261,6 +309,10 @@ github.com/gobuffalo/packd v0.0.0-20181124090624-311c6248e5fb/go.mod h1:Foenia9Z github.com/gobuffalo/packd v0.0.0-20181207120301-c49825f8f6f4/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687 h1:uZ+G4JprR0UEq0aHZs+6eP7TEZuFfrIkmQWejIBV/QQ= github.com/gobuffalo/packd v0.0.0-20181212173646-eca3b8fd6687/go.mod h1:LYc0TGKFBBFTRC9dg2pcRcMqGCTMD7T2BIMP7OBuQAA= +github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4= +github.com/gobuffalo/packd v0.2.0/go.mod h1:k2CkHP3bjbqL2GwxwhxUy1DgnlbW644hkLC9iIUvZwY= +github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4= +github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= github.com/gobuffalo/packr v1.13.7/go.mod h1:KkinLIn/n6+3tVXMwg6KkNvWwVsrRAz4ph+jgpk3Z24= github.com/gobuffalo/packr v1.15.0/go.mod h1:t5gXzEhIviQwVlNx/+3SfS07GS+cZ2hn76WLzPp6MGI= github.com/gobuffalo/packr v1.15.1/go.mod h1:IeqicJ7jm8182yrVmNbM6PR4g79SjN9tZLH8KduZZwE= @@ -278,6 +330,12 @@ github.com/gobuffalo/packr/v2 v2.0.0-rc.13/go.mod h1:2Mp7GhBFMdJlOK8vGfl7SYtfMP3 github.com/gobuffalo/packr/v2 v2.0.0-rc.14/go.mod h1:06otbrNvDKO1eNQ3b8hst+1010UooI2MFg+B2Ze4MV8= github.com/gobuffalo/packr/v2 v2.0.0-rc.15 h1:vSmYcMO6CtuNQvMSbEJeIJlaeZzz2zoxGLTy8HrDh80= github.com/gobuffalo/packr/v2 v2.0.0-rc.15/go.mod h1:IMe7H2nJvcKXSF90y4X1rjYIRlNMJYCxEhssBXNZwWs= +github.com/gobuffalo/packr/v2 v2.4.0/go.mod h1:ra341gygw9/61nSjAbfwcwh8IrYL4WmR4IsPkPBhQiY= +github.com/gobuffalo/packr/v2 v2.5.2/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08= +github.com/gobuffalo/packr/v2 v2.5.3/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08= +github.com/gobuffalo/packr/v2 v2.6.0/go.mod h1:sgEE1xNZ6G0FNN5xn9pevVu4nywaxHvgup67xisti08= +github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o= +github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc= github.com/gobuffalo/plush v3.7.16+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.20+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.21+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= @@ -285,14 +343,22 @@ github.com/gobuffalo/plush v3.7.22+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5s github.com/gobuffalo/plush v3.7.23+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.30+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plush v3.7.31+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= +github.com/gobuffalo/plush v3.7.32+incompatible h1:sxY0WMa6J1pMuomTUQ8n2TlR3otiCVZaq21gSrHLScU= github.com/gobuffalo/plush v3.7.32+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= +github.com/gobuffalo/plush v3.8.2+incompatible h1:EXtDf5L7TTwX8tEddtdHT+PT2lFerIKQm8Ye/M7O+54= +github.com/gobuffalo/plush v3.8.2+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= +github.com/gobuffalo/plush v3.8.3+incompatible h1:kzvUTnFPhwyfPEsx7U7LI05/IIslZVGnAlMA1heWub8= +github.com/gobuffalo/plush v3.8.3+incompatible/go.mod h1:rQ4zdtUUyZNqULlc6bqd5scsPfLKfT0+TGMChgduDvI= github.com/gobuffalo/plushgen v0.0.0-20181128164830-d29dcb966cb2/go.mod h1:r9QwptTFnuvSaSRjpSp4S2/4e2D3tJhARYbvEBcKSb4= github.com/gobuffalo/plushgen v0.0.0-20181203163832-9fc4964505c2/go.mod h1:opEdT33AA2HdrIwK1aibqnTJDVVKXC02Bar/GT1YRVs= github.com/gobuffalo/plushgen v0.0.0-20181207152837-eedb135bd51b/go.mod h1:Lcw7HQbEVm09sAQrCLzIxuhFbB3nAgp4c55E+UlynR0= github.com/gobuffalo/plushgen v0.0.0-20190104222512-177cd2b872b3/go.mod h1:tYxCozi8X62bpZyKXYHw1ncx2ZtT2nFvG42kuLwYjoc= +github.com/gobuffalo/plushgen v0.1.2/go.mod h1:3U71v6HWZpVER1nInTXeAwdoRNsRd4W8aeIa1Lyp+Bk= github.com/gobuffalo/pop v4.8.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop v4.8.3+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/pop v4.8.4+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= +github.com/gobuffalo/pop v4.12.2+incompatible h1:WFHMzzHbVLulZnEium1VlYRnWkzHz39FzVLov6rZdDI= +github.com/gobuffalo/pop v4.12.2+incompatible/go.mod h1:DwBz3SD5SsHpTZiTubcsFWcVDpJWGsxjVjMPnkiThWg= github.com/gobuffalo/release v1.0.35/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.38/go.mod h1:VtHFAKs61vO3wboCec5xr9JPTjYyWYcvaM3lclkc4x4= github.com/gobuffalo/release v1.0.42/go.mod h1:RPs7EtafH4oylgetOJpGP0yCZZUiO4vqHfTHJjSdpug= @@ -308,16 +374,24 @@ github.com/gobuffalo/shoulders v1.0.1/go.mod h1:V33CcVmaQ4gRUmHKwq1fiTXuf8Gp/qjQ github.com/gobuffalo/syncx v0.0.0-20181120191700-98333ab04150/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f h1:S5EeH1reN93KR0L6TQvkRpu9YggCYXrUqFh1iEgvdC0= github.com/gobuffalo/syncx v0.0.0-20181120194010-558ac7de985f/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/tags v2.0.11+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= github.com/gobuffalo/tags v2.0.14+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= +github.com/gobuffalo/tags v2.0.15+incompatible h1:qc5hMXPsXD+zYPSlNgqzlpVW7z/+GJEVCqn245BEUIY= github.com/gobuffalo/tags v2.0.15+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= +github.com/gobuffalo/tags v2.1.0+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= +github.com/gobuffalo/tags v2.1.7+incompatible h1:GUxxh34f9SI4U0Pj3ZqvopO9SlzuqSf+g4ZGSPSszt4= +github.com/gobuffalo/tags v2.1.7+incompatible/go.mod h1:9XmhOkyaB7UzvuY4UoZO4s67q8/xRMVJEaakauVQYeY= github.com/gobuffalo/uuid v2.0.3+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= github.com/gobuffalo/uuid v2.0.4+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= +github.com/gobuffalo/uuid v2.0.5+incompatible h1:c5uWRuEnYggYCrT9AJm0U2v1QTG7OVDAvxhj8tIV5Gc= github.com/gobuffalo/uuid v2.0.5+incompatible/go.mod h1:ErhIzkRhm0FtRuiE/PeORqcw4cVi1RtSpnwYrxuvkfE= +github.com/gobuffalo/validate v2.0.3+incompatible h1:6f4JCEz11Zi6iIlexMv7Jz10RBPvgI795AOaubtCwTE= github.com/gobuffalo/validate v2.0.3+incompatible/go.mod h1:N+EtDe0J8252BgfzQUChBgfd6L93m9weay53EWFVsMM= github.com/gobuffalo/x v0.0.0-20181003152136-452098b06085/go.mod h1:WevpGD+5YOreDJznWevcn8NTmQEW5STSBgIkpkjzqXc= github.com/gobuffalo/x v0.0.0-20181007152206-913e47c59ca7/go.mod h1:9rDPXaB3kXdKWzMc4odGQQdG2e2DIEmANy5aSJ9yesY= github.com/gofrs/uuid v3.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -342,6 +416,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-github/v27 v27.0.1 h1:sSMFSShNn4VnqCqs+qhab6TS3uQc+uVR6TD1bW6MavM= github.com/google/go-github/v27 v27.0.1/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -383,13 +458,47 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0 h1:DUwgMQuuPnS0rhMXenUtZpqZqrR/30NWY+qQvTpSvEs= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc= github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.1.0 h1:10i6DMVJOSko/sD3FLpFKBHONzDGKkX8pbLyHC8B92o= +github.com/jackc/pgconn v1.1.0/go.mod h1:GgY/Lbj1VonNaVdNUHs9AwWom3yP2eymFQ1C8z9r/Lk= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0 h1:FApgMJ/GtaXfI0s8Lvd0kaLaRwMOhs4VH92pwkwQQvU= +github.com/jackc/pgproto3/v2 v2.0.0/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgx v3.2.0+incompatible h1:0Vihzu20St42/UDsvZGdNE6jak7oi/UOeMzwMPHkgFY= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -412,6 +521,9 @@ github.com/karrick/godirwalk v1.7.5/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46s github.com/karrick/godirwalk v1.7.7/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= github.com/karrick/godirwalk v1.7.8 h1:VfG72pyIxgtC7+3X9CMHI0AOl4LwyRAg98WAgsvffi8= github.com/karrick/godirwalk v1.7.8/go.mod h1:2c9FRhkDxdIbgkOnCEvnSWs71Bhugbl46shStcFDJ34= +github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -424,12 +536,14 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/luna-duclos/instrumentedsql v0.0.0-20181127104832-b7d587d28109 h1:SSbnT1UH/TdSedRIy8XVB1dsVUOFP8iHaa/+QE0/q2k= @@ -443,6 +557,7 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3 github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/markbates/deplist v1.0.4/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= github.com/markbates/deplist v1.0.5/go.mod h1:gRRbPbbuA8TmMiRvaOzUlRfzfjeCCBqX2A6arxN01MM= +github.com/markbates/going v1.0.2 h1:uNQHDDfMRNOUmuxDbPbvatyw4wr4UOSUZkGkdkcip1o= github.com/markbates/going v1.0.2/go.mod h1:UWCk3zm0UKefHZ7l8BNqi26UyiEMniznk8naLdTcy6c= github.com/markbates/grift v1.0.4/go.mod h1:wbmtW74veyx+cgfwFhlnnMWqhoz55rnHR47oMXzsyVs= github.com/markbates/hmax v1.0.0 h1:yo2N0gBoCnUMKhV/VRLHomT6Y9wUm+oQQENuWJqCdlM= @@ -450,6 +565,7 @@ github.com/markbates/hmax v1.0.0/go.mod h1:cOkR9dktiESxIMu+65oc/r/bdY4bE8zZw3OLh github.com/markbates/inflect v1.0.0/go.mod h1:oTeZL2KHA7CUX6X+fovmK9OvIOFuqu0TwdQrZjLTh88= github.com/markbates/inflect v1.0.1/go.mod h1:uv3UVNBe5qBIfCm8O8Q+DW+S1EopeyINj+Ikhc7rnCk= github.com/markbates/inflect v1.0.3/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= +github.com/markbates/inflect v1.0.4 h1:5fh1gzTFhfae06u3hzHYO9xe3l3v3nW5Pwt3naLTP5g= github.com/markbates/inflect v1.0.4/go.mod h1:1fR9+pO2KHEO9ZRtto13gDwwZaAKstQzferVeWqbgNs= github.com/markbates/oncer v0.0.0-20180924031910-e862a676800b/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/oncer v0.0.0-20180924034138-723ad0170a46/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= @@ -462,21 +578,32 @@ github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc= github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.11.0 h1:LDdKkqtYlom37fkvqs8rMPFKAMe8+SgjbwZ6ex1/A/Q= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c= +github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2 h1:7eJB6EqsPhRVxvwEXGnqdO2sJI0PTsrWoTMXEk9/OQc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.4 h1:/mdWfiU2y8kZ48EtgByYev/XT3W4dkTuKLOJJsh/r+o= github.com/mattn/goveralls v0.0.4/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -491,13 +618,21 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/nicksnyder/go-i18n v1.10.0/go.mod h1:HrK7VCrbOvQoUAQ7Vpy7i87N7JZZZ7R2xBGjv0j365Q= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oleiade/reflections v1.0.0/go.mod h1:RbATFBbKYkVdqmSFtx13Bb/tVhR0lgOBXunWTZKeL4w= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.9.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.6.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= @@ -522,14 +657,15 @@ github.com/ory/gojsonschema v1.2.0 h1:ePsM9vnsxVHrEHW9/bE2DyU4s34B/YdDtT4LoPnGNs github.com/ory/gojsonschema v1.2.0/go.mod h1:BNZpdJgB74KOLSsWFvzw6roXg1I6O51WO8roMmW+T7Y= github.com/ory/graceful v0.1.1 h1:zx+8tDObLPrG+7Tc8jKYlXsqWnLtOQA1IZ/FAAKHMXU= github.com/ory/graceful v0.1.1/go.mod h1:zqu70l95WrKHF4AZ6tXHvAqAvpY6M7g6ttaAVcMm7KU= -github.com/ory/herodot v0.6.2 h1:zOb5MsuMn7AH9/Ewc/EK83yqcNViK1m1l3C2UuP3RcA= github.com/ory/herodot v0.6.2/go.mod h1:3BOneqcyBsVybCPAJoi92KN2BpJHcmDqAMcAAaJiJow= +github.com/ory/herodot v0.6.3 h1:TPf7uU64t3+kw1DX8GPSSL3GJB92Ql9zQMIBU4D0BfU= +github.com/ory/herodot v0.6.3/go.mod h1:YXKOfAXYdQojDP5sD8m0ajowq3+QXNdtxA+QiUXBwn0= github.com/ory/jsonschema/v2 v2.1.1-0.20191123130340-1c20114d2c04 h1:Yb4pmkauz7cWHFydPJI/yu6EsKGKawY85efES64sW7E= github.com/ory/jsonschema/v2 v2.1.1-0.20191123130340-1c20114d2c04/go.mod h1:yzJzKUGV4RbWqWIBBP4wSOBqavX5saE02yirLS0OTyg= github.com/ory/viper v1.5.6 h1:w4ceGgWwWLzAFYQ7bHaDZmwNsAto2JPVdyQjQnn7VWI= github.com/ory/viper v1.5.6/go.mod h1:TYmpFpKLxjQwvT4f0QPpkOn4sDXU1kDgAwJpgLYiQ28= -github.com/ory/x v0.0.83 h1:MTFG07BJL+CfvDRBA0VbdZBKjGeSChAa1zIn6QCPwiw= -github.com/ory/x v0.0.83/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE= +github.com/ory/x v0.0.84 h1:foL2GSeL9yXBsEU14WcdX95H4Z0TmyodAJwdWkzvtHI= +github.com/ory/x v0.0.84/go.mod h1:RXLPBG7B+hAViONVg0sHwK+U/ie1Y/NeXrq1JcARfoE= github.com/parnurzeal/gorequest v0.2.15/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= @@ -562,19 +698,31 @@ github.com/rogpeppe/go-internal v1.0.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2 h1:J7U/N7eRtzjhs26d6GqMh2HBuXP8/Z64Densiiieafo= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw= +github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7 h1:ID2fzWzRFJcF/xf/8eLN9GW5CXb6NQnKfC+ksTwMNpY= github.com/rubenv/sql-migrate v0.0.0-20190212093014-1007f53448d7/go.mod h1:WS0rl9eEliYI8DPnr3TOwz4439pay+qNgzJoVya/DmY= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/segmentio/analytics-go v3.0.1+incompatible h1:W7T3ieNQjPFMb+SE8SAVYo6mPkKK/Y37wYdiNf5lCVg= github.com/segmentio/analytics-go v3.0.1+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c h1:rsRTAcCR5CeNLkvgBVSjQoDGRRt6kggsE6XYBqCv2KQ= github.com/segmentio/backo-go v0.0.0-20160424052352-204274ad699c/go.mod h1:kJ9mm9YmoWSkk+oQ+5Cj8DEoRCX2JT6As4kEtIIOp1M= +github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516 h1:ofR1ZdrNSkiWcMsRrubK9tb2/SlZVWttAfqUjJi6QYc= github.com/serenize/snaker v0.0.0-20171204205717-a683aaf2d516/go.mod h1:Yow6lPLSAXx2ifx470yD/nUe22Dv5vBvxK/UK9UUTVs= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= @@ -587,12 +735,15 @@ github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8 github.com/sirupsen/logrus v1.1.1/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d h1:yKm7XZV6j9Ev6lojP2XaIshpT4ymkqhMeSghO5Ps00E= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -623,6 +774,7 @@ github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwf github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -663,6 +815,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= @@ -670,9 +823,11 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -690,9 +845,15 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190102171810-8d7daa0c54b3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190103213133-ff983b9c42bc/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c h1:/nJuwDLoL/zrqY6gf57vxC+Pi+pZ8bfhpPkicO5H7W4= +golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -726,11 +887,13 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582 h1:p9xBe/w/OzkeYVKm234g55gMdD1nSIooTir5kV11kfA= -golang.org/x/net v0.0.0-20191014212845-da9a3fd4c582/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191125084936-ffdde1057850 h1:Vq85/r8R9IdcUHmZ0/nQlUg1v15rzvQ2sHdnZAj/x7s= +golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -742,6 +905,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180831094639-fa5fdf94c789/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -765,15 +930,24 @@ golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd h1:3x5uuvBgE6oaXJjCOvpCC1IpgJogqQ+PqGGU3ZxAgII= golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -811,16 +985,27 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190613204242-ed0dc450797f/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8 h1:VZick+NwcqlXXVsD1iFr4Wo6F1FgBbnM4AOMzhwKQ7w= golang.org/x/tools v0.0.0-20190711191110-9a621aea19f8/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a h1:RzzIfXstYPS78k0QViPGpDcTlV+QuYrbxVmsxDHdxTs= golang.org/x/tools v0.0.0-20191105231337-689d0f08e67a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -857,6 +1042,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= @@ -866,11 +1052,13 @@ gopkg.in/go-playground/validator.v9 v9.28.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWd gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/gorp.v1 v1.7.2 h1:j3DWlAyGVv8whO7AcIWznQ2Yj7yJkn34B8s63GViAAw= gopkg.in/gorp.v1 v1.7.2/go.mod h1:Wo3h+DBQZIxATwftsglhdD/62zRFPhGhTiu5jUJmCaw= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/mail.v2 v2.0.0-20180731213649-a0242b2233b4/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.1.9/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.2.2 h1:orlkJ3myw8CN1nVQHBFfloD+L3egixIa4FvUP6RosSA= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -878,8 +1066,8 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/identity/credentials.go b/identity/credentials.go index 32f6d49ab8b..5053461cfc6 100644 --- a/identity/credentials.go +++ b/identity/credentials.go @@ -1,6 +1,11 @@ package identity -import "encoding/json" +import ( + "encoding/json" + "time" + + "github.com/gofrs/uuid" +) // CredentialsType represents several different credential types, like password credentials, passwordless credentials, // and so on. @@ -11,20 +16,74 @@ const ( CredentialsTypeOIDC CredentialsType = "oidc" ) -// Credentials represents a specific credential type -// -// swagger:model identityCredentials -type Credentials struct { - // RequestID discriminates between different credential types. - ID CredentialsType `json:"id"` +type ( + // Credentials represents a specific credential type + // + // swagger:model identityCredentials + Credentials struct { + ID uuid.UUID `json:"-" db:"id"` + + CredentialTypeID uuid.UUID `json:"-" db:"identity_credential_type_id"` - // Identifiers represents a list of unique identifiers this credential type matches. - Identifiers []string `json:"identifiers"` + // Type discriminates between different types of credentials. + Type CredentialsType `json:"type" db:"-"` - // Config contains the concrete credential payload. This might contain the bcrypt-hashed password, or the email - // for passwordless authentication. - // - // type: string - // format: binary - Config json.RawMessage `json:"config"` + // Identifiers represents a list of unique identifiers this credential type matches. + Identifiers []string `json:"identifiers" db:"-"` + + // Config contains the concrete credential payload. This might contain the bcrypt-hashed password, or the email + // for passwordless authentication. + Config json.RawMessage `json:"config" db:"config"` + + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + CredentialIdentifierCollection CredentialIdentifierCollection `json:"-" faker:"-" has_many:"identity_credential_identifiers" fk_id:"identity_credential_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` + } + + // swagger:ignore + CredentialIdentifier struct { + ID uuid.UUID `db:"id"` + Identifier string `db:"identifier"` + // IdentityCredentialsID is a helper struct field for gobuffalo.pop. + IdentityCredentialsID uuid.UUID `json:"-" db:"identity_credential_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` + } + + // swagger:ignore + CredentialsTypeTable struct { + ID uuid.UUID `json:"-" db:"id"` + Name CredentialsType `json:"-" db:"name"` + } + + // swagger:ignore + CredentialsCollection []Credentials + + // swagger:ignore + CredentialIdentifierCollection []CredentialIdentifier +) + +func (c CredentialsTypeTable) TableName() string { + return "identity_credential_types" +} + +func (c CredentialsCollection) TableName() string { + return "identity_credentials" +} + +func (c Credentials) TableName() string { + return "identity_credentials" +} + +func (c CredentialIdentifierCollection) TableName() string { + return "identity_credential_identifiers" +} + +func (c CredentialIdentifier) TableName() string { + return "identity_credential_identifiers" } diff --git a/identity/extension.go b/identity/extension.go index 686662b866d..c29ae648717 100644 --- a/identity/extension.go +++ b/identity/extension.go @@ -44,7 +44,7 @@ func (e *ValidationExtensionIdentifier) Call(value interface{}, config *schema.E cred, ok := e.i.GetCredentials(CredentialsTypePassword) if !ok { cred = &Credentials{ - ID: CredentialsTypePassword, + Type: CredentialsTypePassword, Identifiers: []string{}, Config: json.RawMessage{}, } diff --git a/identity/extension_test.go b/identity/extension_test.go index 426bb4f4e02..dda1f6797a0 100644 --- a/identity/extension_test.go +++ b/identity/extension_test.go @@ -1,7 +1,6 @@ package identity_test import ( - "encoding/json" "net/http" "net/http/httptest" "testing" @@ -25,7 +24,7 @@ func TestValidationExtension(t *testing.T) { v := NewValidator(conf) i := NewIdentity("") - i.Traits = json.RawMessage(`{ + i.Traits = Traits(`{ "email": "foo@bar.com", "names": [ "foobar", diff --git a/identity/handler.go b/identity/handler.go index dd7899b832b..b67d85f82f2 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -3,6 +3,7 @@ package identity import ( "net/http" + "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/pkg/errors" @@ -49,7 +50,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { func (h *Handler) list(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { limit, offset := pagination.Parse(r, 100, 0, 500) - is, err := h.r.IdentityPool().List(r.Context(), limit, offset) + is, err := h.r.IdentityPool().ListIdentities(r.Context(), limit, offset) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -59,7 +60,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, ps httprouter.Par } func (h *Handler) get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - i, err := h.r.IdentityPool().Get(r.Context(), ps.ByName("id")) + i, err := h.r.IdentityPool().GetIdentity(r.Context(), x.ParseUUID(ps.ByName("id"))) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -76,7 +77,11 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, ps httprouter.P } // We do not allow setting credentials using this method - created, err := h.r.IdentityPool().Create(r.Context(), i.CopyWithoutCredentials()) + i.Credentials = nil + // We do not allow setting the ID using this method + i.ID = uuid.Nil + + err := h.r.IdentityPool().CreateIdentity(r.Context(), &i) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -86,9 +91,9 @@ func (h *Handler) create(w http.ResponseWriter, r *http.Request, ps httprouter.P urlx.AppendPaths( h.c.SelfAdminURL(), "identities", - created.ID, + i.ID.String(), ).String(), - created, + &i, ) } @@ -99,19 +104,17 @@ func (h *Handler) update(w http.ResponseWriter, r *http.Request, ps httprouter.P return } - i.ID = ps.ByName("id") - // We do not allow setting credentials using this method - updated, err := h.r.IdentityPool().Update(r.Context(), (&i).CopyWithoutCredentials()) - if err != nil { + i.ID = x.ParseUUID(ps.ByName("id")) + if err := h.r.IdentityPool().UpdateIdentity(r.Context(), &i); err != nil { h.r.Writer().WriteError(w, r, err) return } - h.r.Writer().Write(w, r, updated) + h.r.Writer().Write(w, r, i) } func (h *Handler) delete(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - if err := h.r.IdentityPool().Delete(r.Context(), ps.ByName("id")); err != nil { + if err := h.r.IdentityPool().DeleteIdentity(r.Context(), x.ParseUUID(ps.ByName("id"))); err != nil { h.r.Writer().WriteError(w, r, err) return } diff --git a/identity/handler_test.go b/identity/handler_test.go index 51d2d75d438..5c7b215a701 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -8,7 +8,6 @@ import ( "net/http/httptest" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -22,7 +21,7 @@ import ( ) func TestHandler(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) router := x.NewRouterAdmin() reg.IdentityHandler().RegisterAdminRoutes(router) ts := httptest.NewServer(router) @@ -70,7 +69,7 @@ func TestHandler(t *testing.T) { t.Run("case=should return an empty list", func(t *testing.T) { parsed := get(t, "/identities", http.StatusOK) - require.True(t, parsed.IsArray()) + require.True(t, parsed.IsArray(), "%s", parsed.Raw) assert.Len(t, parsed.Array(), 0) }) @@ -87,48 +86,49 @@ func TestHandler(t *testing.T) { t.Run("case=should fail to create an entity because schema is not validating", func(t *testing.T) { var i identity.Identity - i.Traits = json.RawMessage(`{"bar":123}`) + i.Traits = identity.Traits(`{"bar":123}`) res := send(t, "POST", "/identities", http.StatusBadRequest, &i) assert.Contains(t, res.Get("error.reason").String(), "invalid type") }) t.Run("case=should create an identity without an ID", func(t *testing.T) { var i identity.Identity - i.Traits = json.RawMessage(`{"bar":"baz"}`) + i.Traits = identity.Traits(`{"bar":"baz"}`) res := send(t, "POST", "/identities", http.StatusCreated, &i) assert.NotEmpty(t, res.Get("id").String(), "%s", res.Raw) assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw) assert.Empty(t, res.Get("credentials").String(), "%s", res.Raw) }) - t.Run("case=should create an identity with an ID", func(t *testing.T) { - var i identity.Identity - i.ID = "exists" - i.Traits = json.RawMessage(`{"bar":"baz"}`) + var i identity.Identity + t.Run("case=should create an identity with an ID which is ignored", func(t *testing.T) { + i.ID = x.NewUUID() + i.Traits = identity.Traits(`{"bar":"baz"}`) res := send(t, "POST", "/identities", http.StatusCreated, &i) - assert.EqualValues(t, "exists", res.Get("id").String(), "%s", res.Raw) + assert.NotEqual(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) + + i.ID = x.ParseUUID(res.Get("id").String()) assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw) assert.Empty(t, res.Get("credentials").String(), "%s", res.Raw) assert.EqualValues(t, viper.GetString(configuration.ViperKeyDefaultIdentityTraitsSchemaURL), res.Get("traits_schema_url").String(), "%s", res.Raw) }) t.Run("case=should be able to get the identity", func(t *testing.T) { - res := get(t, "/identities/exists", http.StatusOK) - assert.EqualValues(t, "exists", res.Get("id").String(), "%s", res.Raw) + res := get(t, "/identities/"+i.ID.String(), http.StatusOK) + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw) assert.EqualValues(t, viper.GetString(configuration.ViperKeyDefaultIdentityTraitsSchemaURL), res.Get("traits_schema_url").String(), "%s", res.Raw) assert.Empty(t, res.Get("credentials").String(), "%s", res.Raw) }) t.Run("case=should update an identity and persist the changes", func(t *testing.T) { - var i identity.Identity - i.Traits = json.RawMessage(`{"bar":"baz","foo":"baz"}`) - res := send(t, "PUT", "/identities/exists", http.StatusOK, &i) + i.Traits = identity.Traits(`{"bar":"baz","foo":"baz"}`) + res := send(t, "PUT", "/identities/"+i.ID.String(), http.StatusOK, &i) assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw) assert.EqualValues(t, "baz", res.Get("traits.foo").String(), "%s", res.Raw) - res = get(t, "/identities/exists", http.StatusOK) - assert.EqualValues(t, "exists", res.Get("id").String(), "%s", res.Raw) + res = get(t, "/identities/"+i.ID.String(), http.StatusOK) + assert.EqualValues(t, i.ID.String(), res.Get("id").String(), "%s", res.Raw) assert.EqualValues(t, "baz", res.Get("traits.bar").String(), "%s", res.Raw) }) @@ -140,18 +140,18 @@ func TestHandler(t *testing.T) { t.Run("case=should not be able to update an identity that does not exist yet", func(t *testing.T) { var i identity.Identity - i.ID = uuid.New().String() - i.Traits = json.RawMessage(`{"bar":"baz"}`) - res := send(t, "PUT", "/identities/"+i.ID, http.StatusNotFound, &i) - assert.Contains(t, res.Get("error.reason").String(), "does not exist") + i.ID = x.NewUUID() + i.Traits = identity.Traits(`{"bar":"baz"}`) + res := send(t, "PUT", "/identities/"+i.ID.String(), http.StatusNotFound, &i) + assert.Contains(t, res.Get("error.message").String(), "Unable to locate the resource", "%s", res.Raw) }) t.Run("case=should delete a client and no longer be able to retrieve it", func(t *testing.T) { - remove(t, "/identities/exists", http.StatusNoContent) - _ = get(t, "/identities/exists", http.StatusNotFound) + remove(t, "/identities/"+i.ID.String(), http.StatusNoContent) + _ = get(t, "/identities/"+i.ID.String(), http.StatusNotFound) }) - t.Run("case=should return 404 for non-existing clients", func(t *testing.T) { - remove(t, "/identities/does-not-exist", http.StatusNotFound) + t.Run("case=should return 404 for non-existing identities", func(t *testing.T) { + remove(t, "/identities/"+x.NewUUID().String(), http.StatusNotFound) }) } diff --git a/identity/identity.go b/identity/identity.go index 1ca8fd57366..3d6eb8aa19c 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -1,43 +1,89 @@ package identity import ( + "database/sql/driver" "encoding/json" + "reflect" "sync" + "time" - "github.com/google/uuid" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/kratos/persistence/aliases" + "github.com/ory/kratos/x" ) -// Identity represents an ORY Kratos identity -// -// An identity can be a real human, a service, an IoT device - everything that -// can be described as an "actor" in a system. -// -// swagger:model identity -type Identity struct { - l *sync.RWMutex - - // ID is a unique identifier chosen by you. It can be a URN (e.g. "arn:aws:iam::123456789012"), - // a stringified integer (e.g. "123456789012"), a uuid (e.g. "9f425a8d-7efc-4768-8f23-7647a74fdf13"). It is up to you - // to pick a format you'd like. It is discouraged to use a personally identifiable value here, like the username - // or the email, as this field is immutable. +type ( + // Identity represents an ORY Kratos identity + // + // An identity can be a real human, a service, an IoT device - everything that + // can be described as an "actor" in a system. // - // required: true - ID string `json:"id" faker:"uuid_hyphenated" form:"id" db:"id"` + // swagger:model identity + Identity struct { + l *sync.RWMutex `db:"-" faker:"-"` + + // ID is a unique identifier chosen by you. It can be a URN (e.g. "arn:aws:iam::123456789012"), + // a stringified integer (e.g. "123456789012"), a uuid (e.g. "9f425a8d-7efc-4768-8f23-7647a74fdf13"). It is up to you + // to pick a format you'd like. It is discouraged to use a personally identifiable value here, like the username + // or the email, as this field is immutable. + // + // required: true + ID uuid.UUID `json:"id" faker:"uuid" db:"id" rw:"r"` + + // Credentials represents all credentials that can be used for authenticating this identity. + Credentials map[CredentialsType]Credentials `json:"-" faker:"-" db:"-"` + + // TraitsSchemaURL is the JSON Schema to be used for validating the identity's traits. + // + // format: uri + TraitsSchemaURL string `json:"traits_schema_url,omitempty" faker:"-" db:"traits_schema_url"` + + // Traits represent an identity's traits. The identity is able to create, modify, and delete traits + // in a self-service manner. The input will always be validated against the JSON Schema defined + // in `traits_schema_url`. + // + // required: true + Traits Traits `json:"traits" form:"traits" faker:"-" db:"traits"` + + // CredentialsCollection is a helper struct field for gobuffalo.pop. + CredentialsCollection CredentialsCollection `json:"-" faker:"-" has_many:"identity_credentials" fk_id:"identity_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` + } + Traits json.RawMessage +) - // Credentials represents all credentials that can be used for authenticating this identity. - Credentials map[CredentialsType]Credentials `json:"credentials,omitempty" faker:"-" db:"-"` +func (t *Traits) Scan(value interface{}) error { + return aliases.JSONScan(t, value) +} - // TraitsSchemaURL is the JSON Schema to be used for validating the identity's traits. - // - // format: uri - TraitsSchemaURL string `json:"traits_schema_url,omitempty" form:"-" faker:"-" db:"traits_schema_url"` +func (t *Traits) Value() (driver.Value, error) { + return aliases.JSONValue(t) +} - // Traits represent an identity's traits. The identity is able to create, modify, and delete traits - // in a self-service manner. The input will always be validated against the JSON Schema defined - // in `traits_schema_url`. - // - // required: true - Traits json.RawMessage `json:"traits" form:"traits" faker:"-" db:"traits"` +// MarshalJSON returns m as the JSON encoding of m. +func (t Traits) MarshalJSON() ([]byte, error) { + if t == nil { + return []byte("null"), nil + } + return t, nil +} + +// UnmarshalJSON sets *m to a copy of data. +func (t *Traits) UnmarshalJSON(data []byte) error { + if t == nil { + return errors.New("json.RawMessage: UnmarshalJSON on nil pointer") + } + *t = append((*t)[0:0], data...) + return nil +} + +func (i Identity) TableName() string { + return "identities" } func (i *Identity) lock() *sync.RWMutex { @@ -47,6 +93,33 @@ func (i *Identity) lock() *sync.RWMutex { return i.l } +func (i *Identity) CredentialsEqual(c map[CredentialsType]Credentials) bool { + if len(c) != len(i.Credentials) { + return false + } + + if len(c) == 0 && len(i.Credentials) == 0 { + return true + } + + for k, expect := range i.Credentials { + actual, found := c[k] + if !found { + return false + } + + if string(expect.Config) != string(actual.Config) { + return false + } + + if !reflect.DeepEqual(expect.Identifiers, actual.Identifiers) { + return false + } + } + + return true +} + func (i *Identity) SetCredentials(t CredentialsType, c Credentials) { i.lock().Lock() defer i.lock().Unlock() @@ -54,7 +127,7 @@ func (i *Identity) SetCredentials(t CredentialsType, c Credentials) { i.Credentials = make(map[CredentialsType]Credentials) } - c.ID = t + c.Type = t i.Credentials[t] = c } @@ -62,7 +135,7 @@ func (i *Identity) CopyCredentials() map[CredentialsType]Credentials { result := make(map[CredentialsType]Credentials) for id, credential := range i.Credentials { result[id] = Credentials{ - ID: credential.ID, + Type: credential.Type, Identifiers: append([]string{}, credential.Identifiers...), Config: append([]byte{}, credential.Config...), } @@ -89,9 +162,9 @@ func (i *Identity) CopyWithoutCredentials() *Identity { func NewIdentity(traitsSchemaURL string) *Identity { return &Identity{ - ID: uuid.New().String(), + ID: x.NewUUID(), Credentials: map[CredentialsType]Credentials{}, - Traits: json.RawMessage("{}"), + Traits: Traits(json.RawMessage("{}")), TraitsSchemaURL: traitsSchemaURL, l: new(sync.RWMutex), } diff --git a/identity/identity_test.go b/identity/identity_test.go index 3d0d73e50c4..f49954b4055 100644 --- a/identity/identity_test.go +++ b/identity/identity_test.go @@ -20,7 +20,7 @@ func TestCopyCredentials(t *testing.T) { i := NewIdentity("") i.Credentials = map[CredentialsType]Credentials{ "foo": { - ID: "foo", + Type: "foo", Identifiers: []string{"bar"}, Config: json.RawMessage(`{"foo":"bar"}`), }, @@ -28,7 +28,7 @@ func TestCopyCredentials(t *testing.T) { creds := i.CopyCredentials() creds["bar"] = Credentials{ - ID: "bar", + Type: "bar", Identifiers: []string{"bar"}, Config: json.RawMessage(`{"foo":"bar"}`), } diff --git a/identity/pool.go b/identity/pool.go index c824e403ee5..422beb89a90 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -2,14 +2,18 @@ package identity import ( "context" + "encoding/json" + "fmt" + "testing" - "github.com/google/uuid" - "github.com/pkg/errors" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" - "github.com/ory/herodot" + "github.com/ory/viper" "github.com/ory/kratos/driver/configuration" - "github.com/ory/kratos/schema" + "github.com/ory/kratos/x" ) type ( @@ -19,81 +23,333 @@ type ( // Create creates an identity. It is capable of setting credentials without encoding. Will return an error // if identity exists, backend connectivity is broken, or trait validation fails. - Create(context.Context, *Identity) (*Identity, error) + CreateIdentity(context.Context, *Identity) error - // Create creates an identity. It is capable of setting credentials without encoding. Will return an error - // if identity exists, backend connectivity is broken, or trait validation fails. - List(ctx context.Context, limit, offset int) ([]Identity, error) + ListIdentities(ctx context.Context, limit, offset int) ([]Identity, error) - // UpdateConfidential updates an identities confidential data. It is capable of setting credentials without encoding. Will return an error + // UpdateIdentityConfidential updates an identities confidential data. It is capable of setting credentials without encoding. Will return an error // if identity exists, backend connectivity is broken, or trait validation fails. // // Because this will overwrite credentials you always need to update the identity using `GetClassified`. - UpdateConfidential(context.Context, *Identity, map[CredentialsType]Credentials) (*Identity, error) + UpdateIdentityConfidential(context.Context, *Identity) error // Update updates an identity excluding its confidential data. It is capable of setting credentials without encoding. Will return an error // if identity exists, backend connectivity is broken, or trait validation fails. // // This update procedure works well with `Get`. - Update(context.Context, *Identity) (*Identity, error) + UpdateIdentity(context.Context, *Identity) error // Delete removes an identity by its id. Will return an error - // // if identity exists, backend connectivity is broken, or trait validation fails. - Delete(context.Context, string) error + // if identity exists, backend connectivity is broken, or trait validation fails. + DeleteIdentity(context.Context, uuid.UUID) error // Get returns an identity by its id. Will return an error if the identity does not exist or backend // connectivity is broken. - Get(context.Context, string) (*Identity, error) + GetIdentity(context.Context, uuid.UUID) (*Identity, error) // GetClassified returns the identity including it's raw credentials. This should only be used internally. - GetClassified(_ context.Context, id string) (*Identity, error) + GetIdentityConfidential(context.Context, uuid.UUID) (*Identity, error) } PoolProvider interface { IdentityPool() Pool } - - abstractPool struct { - c configuration.Provider - d ValidationProvider - } ) -func newAbstractPool(c configuration.Provider, d ValidationProvider) *abstractPool { - return &abstractPool{c: c, d: d} -} +func TestPool(p Pool) func(t *testing.T) { + return func(t *testing.T) { + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") -func (p *abstractPool) augment(i Identity) *Identity { - if i.ID == "" { - i.ID = uuid.New().String() - } + var createdIDs []uuid.UUID - if i.TraitsSchemaURL == "" { - i.TraitsSchemaURL = p.c.DefaultIdentityTraitsSchemaURL().String() - } + var passwordIdentity = func(schemaURL string, credentialsID string) *Identity { + i := NewIdentity(schemaURL) + i.SetCredentials(CredentialsTypePassword, Credentials{ + Type: CredentialsTypePassword, Identifiers: []string{credentialsID}, + Config: json.RawMessage(`{"foo":"bar"}`), + }) + return i + } - return &i -} + var oidcIdentity = func(schemaURL string, credentialsID string) *Identity { + i := NewIdentity(schemaURL) + i.SetCredentials(CredentialsTypeOIDC, Credentials{ + Type: CredentialsTypeOIDC, Identifiers: []string{credentialsID}, + Config: json.RawMessage(`{}`), + }) + return i + } -func (p *abstractPool) declassify(i Identity) *Identity { - return i.CopyWithoutCredentials() -} + var assertEqual = func(t *testing.T, expected, actual *Identity) { + assert.Empty(t, actual.Credentials) + require.Equal(t, expected.Traits, actual.Traits) + require.Equal(t, expected.ID, actual.ID) + } -func (p *abstractPool) declassifyAll(i []Identity) []Identity { - declassified := make([]Identity, len(i)) - for k, ii := range i { - declassified[k] = *ii.CopyWithoutCredentials() - } - return declassified -} + t.Run("case=should create and set missing ID", func(t *testing.T) { + i := NewIdentity("") + i.SetCredentials(CredentialsTypeOIDC, Credentials{ + Type: CredentialsTypeOIDC, Identifiers: []string{x.NewUUID().String()}, + Config: json.RawMessage(`{}`), + }) + i.ID = uuid.Nil + require.NoError(t, p.CreateIdentity(context.Background(), i)) + assert.NotEqual(t, uuid.Nil, i.ID) + createdIDs = append(createdIDs, i.ID) + }) -func (p *abstractPool) Validate(i *Identity) error { - if err := p.d.IdentityValidator().Validate(i); err != nil { - if _, ok := errors.Cause(err).(schema.ResultErrors); ok { - return errors.WithStack(herodot.ErrBadRequest.WithReasonf("%s", err)) - } - return err - } + t.Run("case=create with default values", func(t *testing.T) { + expected := passwordIdentity("", "id-1") + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + createdIDs = append(createdIDs, expected.ID) + + actual, err := p.GetIdentity(context.Background(), expected.ID) + require.NoError(t, err) + + assert.Equal(t, expected.ID, actual.ID) + assert.Equal(t, "file://./stub/identity.schema.json", actual.TraitsSchemaURL) + assertEqual(t, expected, actual) + }) + + t.Run("case=should error when the identity ID does not exist", func(t *testing.T) { + _, err := p.GetIdentity(context.Background(), uuid.UUID{}) + require.Error(t, err) + + _, err = p.GetIdentity(context.Background(), x.NewUUID()) + require.Error(t, err) + + _, err = p.GetIdentityConfidential(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + t.Run("case=create and keep set values", func(t *testing.T) { + expected := passwordIdentity("file://./stub/identity-2.schema.json", "id-2") + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + createdIDs = append(createdIDs, expected.ID) + + actual, err := p.GetIdentity(context.Background(), expected.ID) + require.NoError(t, err) + assert.Equal(t, "file://./stub/identity-2.schema.json", actual.TraitsSchemaURL) + assertEqual(t, expected, actual) + + actual, err = p.GetIdentityConfidential(context.Background(), expected.ID) + require.NoError(t, err) + require.Equal(t, expected.Traits, actual.Traits) + require.Equal(t, expected.ID, actual.ID) + + assert.Empty(t, actual.CredentialsCollection) + assert.NotEmpty(t, actual.Credentials) + assert.NotEmpty(t, expected.Credentials) + + for m, expected := range expected.Credentials { + assert.Equal(t, expected.ID, actual.Credentials[m].ID) + assert.JSONEq(t, string(expected.Config), string(actual.Credentials[m].Config)) + assert.Equal(t, expected.Identifiers, actual.Credentials[m].Identifiers) + assert.Equal(t, expected.Type, actual.Credentials[m].Type) + } + }) + + t.Run("case=fail on duplicate credential identifiers if type is password", func(t *testing.T) { + initial := passwordIdentity("", "foo@bar.com") + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + for _, ids := range []string{"foo@bar.com", "fOo@bar.com", "FOO@bar.com", "foo@Bar.com"} { + expected := passwordIdentity("", ids) + require.Error(t, p.CreateIdentity(context.Background(), expected)) + + _, err := p.GetIdentity(context.Background(), expected.ID) + require.Error(t, err) + } + }) + + t.Run("case=fail on duplicate credential identifiers if type is oidc", func(t *testing.T) { + initial := oidcIdentity("", "oidc-1") + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + expected := oidcIdentity("", "oidc-1") + require.Error(t, p.CreateIdentity(context.Background(), expected)) + + _, err := p.GetIdentity(context.Background(), expected.ID) + require.Error(t, err) - return nil + second := oidcIdentity("", "OIDC-1") + require.NoError(t, p.CreateIdentity(context.Background(), second), "should work because oidc is not case-sensitive") + createdIDs = append(createdIDs, second.ID) + }) + + t.Run("case=create with invalid traits data", func(t *testing.T) { + expected := oidcIdentity("", x.NewUUID().String()) + expected.Traits = Traits(`{"bar":123}`) // bar should be a string + err := p.CreateIdentity(context.Background(), expected) + require.Error(t, err) + assert.Contains(t, fmt.Sprintf("%+v", err.Error()), "malformed") + }) + + t.Run("case=get classified credentials", func(t *testing.T) { + initial := oidcIdentity("", x.NewUUID().String()) + initial.SetCredentials(CredentialsTypeOIDC, Credentials{ + Type: CredentialsTypeOIDC, Identifiers: []string{"aylmao-oidc"}, + Config: json.RawMessage(`{"ay":"lmao"}`), + }) + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + initial, err := p.GetIdentityConfidential(context.Background(), initial.ID) + require.NoError(t, err) + require.NotEqual(t, uuid.Nil, initial.ID) + require.NotEmpty(t, initial.Credentials) + }) + + t.Run("case=fail to update an identity because credentials changed but update was called", func(t *testing.T) { + initial := oidcIdentity("", x.NewUUID().String()) + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + assert.Equal(t, "file://./stub/identity.schema.json", initial.TraitsSchemaURL) + + toUpdate := initial.CopyWithoutCredentials() + toUpdate.SetCredentials(CredentialsTypePassword, Credentials{ + Type: CredentialsTypePassword, + Identifiers: []string{"ignore-me"}, + Config: json.RawMessage(`{"oh":"nono"}`), + }) + toUpdate.Traits = Traits(`{"update":"me"}`) + toUpdate.TraitsSchemaURL = "file://./stub/identity-2.schema.json" + + err := p.UpdateIdentity(context.Background(), toUpdate) + require.Error(t, err) + assert.Contains(t, fmt.Sprintf("%+v", err), "A field was modified that updates one or more credentials-related settings.") + + actual, err := p.GetIdentityConfidential(context.Background(), toUpdate.ID) + require.NoError(t, err) + assert.Equal(t, "file://./stub/identity.schema.json", actual.TraitsSchemaURL) + assert.Empty(t, actual.Credentials[CredentialsTypePassword]) + assert.NotEmpty(t, actual.Credentials[CredentialsTypeOIDC]) + }) + + t.Run("case=update an identity and set credentials", func(t *testing.T) { + initial := oidcIdentity("", x.NewUUID().String()) + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + assert.Equal(t, "file://./stub/identity.schema.json", initial.TraitsSchemaURL) + + expected := initial.CopyWithoutCredentials() + expected.SetCredentials(CredentialsTypePassword, Credentials{ + Type: CredentialsTypePassword, + Identifiers: []string{"ignore-me"}, + Config: json.RawMessage(`{"oh":"nono"}`), + }) + expected.Traits = Traits(`{"update":"me"}`) + expected.TraitsSchemaURL = "file://./stub/identity-2.schema.json" + require.NoError(t, p.UpdateIdentityConfidential(context.Background(), expected)) + + actual, err := p.GetIdentityConfidential(context.Background(), expected.ID) + require.NoError(t, err) + assert.Equal(t, "file://./stub/identity-2.schema.json", actual.TraitsSchemaURL) + assert.NotEmpty(t, actual.Credentials[CredentialsTypePassword]) + assert.Empty(t, actual.Credentials[CredentialsTypeOIDC]) + + assert.Equal(t, expected.Credentials[CredentialsTypeOIDC], actual.Credentials[CredentialsTypeOIDC]) + }) + + t.Run("case=fail to update because validation fails", func(t *testing.T) { + initial := oidcIdentity("", x.NewUUID().String()) + + require.NoError(t, p.CreateIdentity(context.Background(), initial)) + createdIDs = append(createdIDs, initial.ID) + + initial.Traits = Traits(`{"bar":123}`) + err := p.UpdateIdentity(context.Background(), initial) + require.Error(t, err) + require.Contains(t, err.Error(), "malformed") + }) + + t.Run("case=should fail to insert identity because credentials from traits exist", func(t *testing.T) { + first := passwordIdentity("", x.NewUUID().String()) + first.Traits = Traits(`{"email":"test-identity@ory.sh"}`) + require.NoError(t, p.CreateIdentity(context.Background(), first)) + createdIDs = append(createdIDs, first.ID) + + second := passwordIdentity("", x.NewUUID().String()) + require.NoError(t, p.CreateIdentity(context.Background(), second)) + createdIDs = append(createdIDs, second.ID) + + second.Traits = Traits(`{"email":"test-identity@ory.sh"}`) + require.Error(t, p.UpdateIdentityConfidential(context.Background(), second)) + }) + + t.Run("case=should succeed to update credentials from traits", func(t *testing.T) { + expected := passwordIdentity("", x.NewUUID().String()) + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + createdIDs = append(createdIDs, expected.ID) + + expected.Traits = Traits(`{"email":"update-test-identity@ory.sh"}`) + require.NoError(t, p.UpdateIdentityConfidential(context.Background(), expected)) + + actual, err := p.GetIdentityConfidential(context.Background(), expected.ID) + require.NoError(t, err) + + assert.Equal(t, expected.Credentials[CredentialsTypePassword].Identifiers, actual.Credentials[CredentialsTypePassword].Identifiers) + }) + + t.Run("case=delete an identity", func(t *testing.T) { + expected := passwordIdentity("", x.NewUUID().String()) + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + require.NoError(t, p.DeleteIdentity(context.Background(), expected.ID)) + + _, err := p.GetIdentity(context.Background(), expected.ID) + require.Error(t, err) + }) + + t.Run("case=create with empty credentials config", func(t *testing.T) { + // This test covers a case where the config value of a credentials setting is empty. This causes + // issues with postgres' json field. + expected := passwordIdentity("", x.NewUUID().String()) + expected.SetCredentials(CredentialsTypePassword, Credentials{ + Type: CredentialsTypePassword, + Identifiers: []string{"id-missing-creds-config"}, + Config: json.RawMessage(``), + }) + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + createdIDs = append(createdIDs, expected.ID) + }) + + t.Run("case=list", func(t *testing.T) { + is, err := p.ListIdentities(context.Background(), 25, 0) + require.NoError(t, err) + assert.Len(t, is, len(createdIDs)) + for _, id := range createdIDs { + var found bool + for _, i := range is { + if i.ID == id { + found = true + } + } + assert.True(t, found, id) + } + }) + + t.Run("case=find identity by its credentials identifier", func(t *testing.T) { + expected := passwordIdentity("file://./stub/identity.schema.json", x.NewUUID().String()) + expected.Traits = Traits(`{"email": "find-credentials-identifier@ory.sh"}`) + + require.NoError(t, p.CreateIdentity(context.Background(), expected)) + createdIDs = append(createdIDs, expected.ID) + + actual, creds, err := p.FindByCredentialsIdentifier(context.Background(), CredentialsTypePassword, "find-credentials-identifier@ory.sh") + require.NoError(t, err) + + assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].ID, creds.ID) + assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].Identifiers, creds.Identifiers) + assert.JSONEq(t, string(expected.Credentials[CredentialsTypePassword].Config), string(creds.Config)) + // assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].CreatedAt.Unix(), creds.CreatedAt.Unix()) + // assert.EqualValues(t, expected.Credentials[CredentialsTypePassword].UpdatedAt.Unix(), creds.UpdatedAt.Unix()) + + expected.Credentials = nil + assertEqual(t, expected, actual) + }) + } } diff --git a/identity/pool_memory.go b/identity/pool_memory.go deleted file mode 100644 index 4f9350470fb..00000000000 --- a/identity/pool_memory.go +++ /dev/null @@ -1,199 +0,0 @@ -package identity - -import ( - "context" - "sync" - - "github.com/pkg/errors" - - "github.com/ory/go-convenience/stringslice" - "github.com/ory/herodot" - "github.com/ory/x/pagination" - - "github.com/ory/kratos/driver/configuration" - "github.com/ory/kratos/schema" -) - -var _ Pool = new(PoolMemory) - -type PoolMemory struct { - *abstractPool - sync.RWMutex - - is []Identity -} - -func NewPoolMemory(c configuration.Provider, d ValidationProvider) *PoolMemory { - return &PoolMemory{ - abstractPool: newAbstractPool(c, d), - is: make([]Identity, 0), - } -} - -func (p *PoolMemory) hasConflictingID(i *Identity) bool { - p.RLock() - defer p.RUnlock() - - for _, fromPool := range p.is { - if fromPool.ID == i.ID { - return true - } - } - return false -} - -func (p *PoolMemory) hasConflictingCredentials(i *Identity) bool { - p.RLock() - defer p.RUnlock() - - for _, fromPool := range p.is { - if fromPool.ID == i.ID { - continue - } - - for fromPoolID, fromPoolCredentials := range fromPool.Credentials { - for credentialsID, cc := range i.Credentials { - if fromPoolID == credentialsID { - for _, identifier := range cc.Identifiers { - if stringslice.Has(fromPoolCredentials.Identifiers, identifier) { - return true - } - } - } - } - } - } - return false -} - -// FindByCredentialsIdentifier returns an identity by querying for it's credential identifiers. -func (p *PoolMemory) FindByCredentialsIdentifier(_ context.Context, ct CredentialsType, match string) (*Identity, *Credentials, error) { - p.RLock() - defer p.RUnlock() - - for _, i := range p.is { - for ctid, c := range i.Credentials { - if ct == ctid { - if stringslice.Has(c.Identifiers, match) { - return p.declassify(i), &c, nil - } - } - } - } - return nil, nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("No identity matching the credentials identifiers")) -} - -func (p *PoolMemory) Create(_ context.Context, i *Identity) (*Identity, error) { - insert := p.augment(*i) - if err := p.Validate(insert); err != nil { - return nil, err - } - - if p.hasConflictingID(insert) { - return nil, errors.WithStack(herodot.ErrConflict.WithReasonf("An identity with the given ID exists already.")) - } - - if p.hasConflictingCredentials(insert) { - return nil, errors.WithStack(schema.NewDuplicateCredentialsError()) - } - - p.Lock() - p.is = append(p.is, *insert) - p.Unlock() - - return p.abstractPool.declassify(*insert), nil -} - -func (p *PoolMemory) List(_ context.Context, limit, offset int) ([]Identity, error) { - p.RLock() - defer p.RUnlock() - - start, end := pagination.Index(limit, offset, len(p.is)) - identities := make([]Identity, limit) - for k, i := range p.is[start:end] { - identities[k] = *p.declassify(i) - } - - return p.abstractPool.declassifyAll(p.is[start:end]), nil -} - -func (p *PoolMemory) UpdateConfidential(ctx context.Context, i *Identity, ct map[CredentialsType]Credentials) (*Identity, error) { - return p.update(ctx, i, ct, true) -} - -func (p *PoolMemory) Update(ctx context.Context, i *Identity) (*Identity, error) { - return p.update(ctx, i, nil, false) -} - -func (p *PoolMemory) update(ctx context.Context, i *Identity, ct map[CredentialsType]Credentials, updateCredentials bool) (*Identity, error) { - insert := p.augment(*i) - insert.Credentials = ct - if err := p.Validate(insert); err != nil { - return nil, err - } - - if updateCredentials && p.hasConflictingCredentials(insert) { - return nil, errors.WithStack(schema.NewDuplicateCredentialsError()) - } - - p.RLock() - for k, ii := range p.is { - if ii.ID == insert.ID { - p.RUnlock() - - p.Lock() - if !updateCredentials { - insert.Credentials = ii.Credentials - } - p.is[k] = *insert - p.Unlock() - - return p.declassify(*insert), nil - } - } - p.RUnlock() - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Identity with identifier %s does not exist.", i.ID)) -} - -func (p *PoolMemory) Get(ctx context.Context, id string) (*Identity, error) { - i, err := p.GetClassified(ctx, id) - if err != nil { - return nil, err - } - - return p.declassify(*i), nil -} - -func (p *PoolMemory) GetClassified(_ context.Context, id string) (*Identity, error) { - p.RLock() - defer p.RUnlock() - - for _, ii := range p.is { - if ii.ID == id { - return &ii, nil - } - } - - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Identity with identifier %s does not exist.", id)) -} - -func (p *PoolMemory) Delete(_ context.Context, id string) error { - p.Lock() - defer p.Unlock() - - offset := -1 - for k, ii := range p.is { - if ii.ID == id { - offset = k - break - } - } - - if offset == -1 { - return errors.WithStack(herodot.ErrNotFound.WithReasonf("Identity with identifier %s does not exist.", id)) - } - - p.is = append(p.is[:offset], p.is[offset+1:]...) - - return nil -} diff --git a/identity/pool_sql.go b/identity/pool_sql.go deleted file mode 100644 index 1f54767e376..00000000000 --- a/identity/pool_sql.go +++ /dev/null @@ -1,282 +0,0 @@ -package identity - -import ( - "context" - "database/sql" - "encoding/json" - "fmt" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/ory/x/stringsx" - - "github.com/ory/x/sqlcon" - "github.com/ory/x/sqlxx" - - "github.com/ory/herodot" - - "github.com/ory/kratos/driver/configuration" -) - -var _ Pool = new(PoolSQL) - -type ( - PoolSQL struct { - *abstractPool - db *sqlx.DB - } -) - -func NewPoolSQL(c configuration.Provider, d ValidationProvider, db *sqlx.DB) *PoolSQL { - return &PoolSQL{abstractPool: newAbstractPool(c, d), db: db} -} - -// FindByCredentialsIdentifier returns an identity by querying for it's credential identifiers. -func (p *PoolSQL) FindByCredentialsIdentifier(ctx context.Context, ct CredentialsType, match string) (*Identity, *Credentials, error) { - i, err := p.get(ctx, "WHERE ici.identifier = ? AND ic.method = ?", []interface{}{match, string(ct)}) - if err != nil { - if errors.Cause(err).Error() == herodot.ErrNotFound.Error() { - return nil, nil, herodot.ErrNotFound.WithTrace(err).WithReasonf(`No identity matching credentials identifier "%s" could be found.`, match) - } - return nil, nil, err - } - - creds, ok := i.Credentials[ct] - if !ok { - return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The SQL adapter failed to return the appropriate credentials_type \"%s\". This is a bug in the code.", ct)) - } - - return p.declassify(*i), &creds, nil -} - -func (p *PoolSQL) Create(ctx context.Context, i *Identity) (*Identity, error) { - insert := p.augment(*i) - if err := p.Validate(insert); err != nil { - return nil, err - } - - tx, err := p.db.BeginTxx(ctx, nil) - if err != nil { - return nil, err - } - - if err := p.insert(ctx, tx, insert); err != nil { - if err := tx.Rollback(); err != nil { - return nil, errors.WithStack(err) - } - return nil, err - } - - if err := tx.Commit(); err != nil { - if err := tx.Rollback(); err != nil { - return nil, errors.WithStack(err) - } - return nil, errors.WithStack(err) - } - - return p.abstractPool.declassify(*insert), nil -} - -func (p *PoolSQL) List(ctx context.Context, limit, offset int) ([]Identity, error) { - var rows []struct { - ID string `db:"id"` - TraitsSchemaURL string `db:"traits_schema_url"` - Traits json.RawMessage `db:"traits"` - } - - query := "SELECT id, traits, traits_schema_url FROM identity ORDER BY pk LIMIT ? OFFSET ?" - if err := p.db.SelectContext(ctx, &rows, p.db.Rebind(query), limit, offset); err != nil { - return nil, sqlcon.HandleError(err) - } - - ids := make([]Identity, len(rows)) - for k, row := range rows { - ids[k] = Identity{ - ID: row.ID, - TraitsSchemaURL: row.TraitsSchemaURL, - Traits: row.Traits, - } - } - - return p.declassifyAll(ids), nil -} - -func (p *PoolSQL) UpdateConfidential(ctx context.Context, i *Identity, ct map[CredentialsType]Credentials) (*Identity, error) { - return p.update(ctx, i, ct, true) -} - -func (p *PoolSQL) Update(ctx context.Context, i *Identity) (*Identity, error) { - return p.update(ctx, i, nil, false) -} - -func (p *PoolSQL) Get(ctx context.Context, id string) (*Identity, error) { - i, err := p.GetClassified(ctx, id) - if err != nil { - return nil, err - } - - return p.declassify(*i), nil -} - -func (p *PoolSQL) GetClassified(ctx context.Context, id string) (*Identity, error) { - i, err := p.get(ctx, "WHERE i.id = ?", []interface{}{id}) - if err != nil { - if errors.Cause(err).Error() == herodot.ErrNotFound.Error() { - return nil, herodot.ErrNotFound.WithTrace(err).WithReasonf(`Identity "%s" could not be found.`, id) - } - return nil, err - } - return i, nil -} - -func (p *PoolSQL) Delete(ctx context.Context, id string) error { - _, err := p.db.ExecContext(ctx, p.db.Rebind("DELETE FROM identity WHERE id = ?"), id) - return sqlcon.HandleError(err) -} - -func (p *PoolSQL) insert(ctx context.Context, tx *sqlx.Tx, i *Identity) error { - columns, arguments := sqlxx.NamedInsertArguments(i) - query := fmt.Sprintf(`INSERT INTO identity (%s) VALUES (%s)`, columns, arguments) - if _, err := tx.NamedExecContext(context.Background(), p.db.Rebind(query), i); err != nil { - return sqlcon.HandleError(err) - } - - return p.insertCredentials(ctx, tx, i) -} - -func (p *PoolSQL) insertCredentials(ctx context.Context, tx *sqlx.Tx, i *Identity) error { - - for method, cred := range i.Credentials { - query := `INSERT INTO identity_credential (method, config, identity_pk) VALUES ( - ?, - ?, - (SELECT pk FROM identity WHERE id = ?))` - if _, err := tx.ExecContext(ctx, - p.db.Rebind(query), - string(method), - stringsx.Coalesce(string(cred.Config), "{}"), - i.ID, - ); err != nil { - return sqlcon.HandleError(err) - } - - for _, identifier := range cred.Identifiers { - query = `INSERT INTO identity_credential_identifier (identifier, identity_credential_pk) VALUES ( - ?, - (SELECT ic.pk FROM identity_credential as ic JOIN identity as i ON (i.pk = ic.identity_pk) WHERE ic.method = ? AND i.id = ?))` - if _, err := tx.ExecContext(ctx, - p.db.Rebind(query), - identifier, - string(method), - i.ID, - ); err != nil { - return sqlcon.HandleError(err) - } - } - } - return nil -} - -func (p *PoolSQL) update(ctx context.Context, i *Identity, ct map[CredentialsType]Credentials, updateConfidential bool) (*Identity, error) { - insert := p.augment(*i) - insert.Credentials = ct - - if err := p.Validate(insert); err != nil { - return nil, err - } - - tx, err := p.db.BeginTxx(ctx, nil) - if err != nil { - return nil, err - } - - if err := p.runUpdateTx(ctx, tx, i, updateConfidential); err != nil { - if err := tx.Rollback(); err != nil { - return nil, errors.WithStack(err) - } - return nil, errors.WithStack(err) - } - - if err := tx.Commit(); err != nil { - if err := tx.Rollback(); err != nil { - return nil, errors.WithStack(err) - } - return nil, errors.WithStack(err) - } - - return p.declassify(*i), nil -} - -func (p *PoolSQL) runUpdateTx(ctx context.Context, tx *sqlx.Tx, i *Identity, updateConfidential bool) error { - arguments := sqlxx.NamedUpdateArguments(i, "id") - query := fmt.Sprintf(`UPDATE identity SET %s WHERE id=:id`, arguments) - if _, err := tx.NamedExecContext(context.Background(), query, i); err != nil { - return sqlcon.HandleError(err) - } - - if !updateConfidential { - return nil - } - - if _, err := tx.ExecContext(ctx, p.db.Rebind(`DELETE FROM identity_credential as ic USING identity as i WHERE i.pk = ic.identity_pk AND i.id = ?`), i.ID); err != nil { - return sqlcon.HandleError(err) - } - return p.insertCredentials(ctx, tx, i) -} - -func (p *PoolSQL) get(ctx context.Context, where string, args []interface{}) (*Identity, error) { - var rows []struct { - ID string `db:"id"` - TraitsSchemaURL string `db:"traits_schema_url"` - Traits json.RawMessage `db:"traits"` - Identifier sql.NullString `db:"identifier"` - Method sql.NullString `db:"method"` - Config sql.NullString `db:"config"` - } - - query := fmt.Sprintf(` -SELECT - i.id as id, i.traits_schema_url as traits_schema_url, i.traits as traits, ic.config as config, ic.method as method, ici.identifier as identifier -FROM identity as i -LEFT OUTER JOIN identity_credential as ic ON - ic.identity_pk = i.pk -LEFT OUTER JOIN identity_credential_identifier as ici ON - ici.identity_credential_pk = ic.pk -%s`, where) - if err := sqlcon.HandleError(p.db.SelectContext(ctx, &rows, p.db.Rebind(query), args...)); err != nil { - if errors.Cause(err) == sqlcon.ErrNoRows { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`Identity could not be found.`)) - } - return nil, err - } - - if len(rows) == 0 { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf(`Identity could not be found.`)) - } - - credentials := map[CredentialsType]Credentials{} - for _, row := range rows { - if !(row.Method.Valid && row.Identifier.Valid && row.Config.Valid) { - continue - } - - if c, ok := credentials[CredentialsType(row.Method.String)]; ok { - c.Identifiers = append(c.Identifiers, row.Identifier.String) - credentials[CredentialsType(row.Method.String)] = c - } else { - credentials[CredentialsType(row.Method.String)] = Credentials{ - ID: CredentialsType(row.Method.String), - Config: json.RawMessage(row.Config.String), - Identifiers: []string{row.Identifier.String}, - } - } - } - - return &Identity{ - ID: rows[0].ID, - TraitsSchemaURL: rows[0].TraitsSchemaURL, - Traits: rows[0].Traits, - Credentials: credentials, - }, nil -} diff --git a/identity/pool_test.go b/identity/pool_test.go deleted file mode 100644 index 7f415660fcd..00000000000 --- a/identity/pool_test.go +++ /dev/null @@ -1,269 +0,0 @@ -package identity_test - -import ( - "context" - "encoding/json" - "flag" - "sync" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/ory/viper" - - "github.com/ory/x/sqlcon/dockertest" - - "github.com/ory/herodot" - - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/driver/configuration" - . "github.com/ory/kratos/identity" - "github.com/ory/kratos/internal" -) - -func init() { - internal.RegisterFakes() -} - -// nolint: staticcheck -func TestMain(m *testing.M) { - flag.Parse() - runner := dockertest.Register() - runner.Exit(m.Run()) -} - -func TestPool(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) - pools := map[string]Pool{ - "memory": NewPoolMemory(conf, reg), - } - - if !testing.Short() { - var l sync.Mutex - dockertest.Parallel([]func(){ - func() { - db, err := dockertest.ConnectToTestPostgreSQL() - require.NoError(t, err) - - _, reg := internal.NewRegistrySQL(t, db) - - l.Lock() - pools["postgres"] = reg.IdentityPool() - l.Unlock() - }, - }) - } - - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") - - var newid = func(schemaURL string, credentialsID string) *Identity { - i := NewIdentity(schemaURL) - i.SetCredentials(CredentialsTypePassword, Credentials{ - ID: CredentialsTypePassword, Identifiers: []string{credentialsID}, - Config: json.RawMessage(`{}`), - }) - return i - } - - var assertEqual = func(t *testing.T, expected, actual *Identity) { - assert.Empty(t, actual.Credentials) - require.Equal(t, expected.Traits, actual.Traits) - require.Equal(t, expected.ID, actual.ID) - } - - for name, pool := range pools { - t.Run("dbal="+name, func(t *testing.T) { - t.Run("case=get-not-exist", func(t *testing.T) { - _, err := pool.Get(context.Background(), "does-not-exist") - require.EqualError(t, err, herodot.ErrNotFound.Error(), "%+v", err) - }) - - t.Run("case=create with default values", func(t *testing.T) { - i := newid("", "id-1") - i.ID = "id-1" - - got, err := pool.Create(context.Background(), i) - require.NoError(t, err) - require.Empty(t, got.Credentials) - - got, err = pool.Get(context.Background(), i.ID) - require.NoError(t, err) - - assert.Equal(t, "file://./stub/identity.schema.json", got.TraitsSchemaURL) - assertEqual(t, i, got) - }) - - t.Run("case=create and keep set values", func(t *testing.T) { - i := newid("file://./stub/identity-2.schema.json", "id-2") - i.ID = "id-2" - - _, err := pool.Create(context.Background(), i) - require.NoError(t, err) - - got, err := pool.Get(context.Background(), i.ID) - require.NoError(t, err) - assert.Equal(t, "file://./stub/identity-2.schema.json", got.TraitsSchemaURL) - assertEqual(t, i, got) - }) - - t.Run("case=fail on duplicate credential identifiers", func(t *testing.T) { - i := newid("", "id-1") - - _, err := pool.Create(context.Background(), i) - require.Error(t, err) - - _, err = pool.Get(context.Background(), i.ID) - require.Error(t, err) - }) - - t.Run("case=create with invalid traits data", func(t *testing.T) { - i := newid("", "id-3") - i.Traits = json.RawMessage(`{"bar":123}`) // bar should be a string - - _, err := pool.Create(context.Background(), i) - require.Error(t, err) - }) - - t.Run("case=update an identity", func(t *testing.T) { - toUpdate, err := pool.GetClassified(context.Background(), "id-1") - require.NoError(t, err) - require.NotEmpty(t, toUpdate.ID) - require.NotEmpty(t, toUpdate.Credentials) - - toUpdate.TraitsSchemaURL = "file://./stub/identity-2.schema.json" - toUpdate, err = pool.UpdateConfidential(context.Background(), toUpdate, toUpdate.Credentials) - require.NoError(t, err) - require.Empty(t, toUpdate.Credentials) - - updatedConfidential, err := pool.GetClassified(context.Background(), "id-1") - require.NoError(t, err, "%+v", toUpdate) - assert.Equal(t, "file://./stub/identity-2.schema.json", updatedConfidential.TraitsSchemaURL) - assert.NotEmpty(t, updatedConfidential.Credentials) - - updatedConfidential.Traits = json.RawMessage(`{"bar":"bazbar"}`) - toUpdate, err = pool.Update(context.Background(), toUpdate) - require.NoError(t, err) - - updatedWithoutCredentials, err := pool.GetClassified(context.Background(), "id-1") - require.NoError(t, err, "%+v", toUpdate) - assert.Equal(t, "file://./stub/identity-2.schema.json", updatedConfidential.TraitsSchemaURL) - assert.Equal(t, `{"bar":"bazbar"}`, string(updatedConfidential.Traits)) - assert.NotEmpty(t, updatedConfidential.Credentials) - assert.Equal(t, updatedConfidential.Credentials, updatedWithoutCredentials.Credentials) - }) - - t.Run("case=fail to update because validation fails", func(t *testing.T) { - got, err := pool.Get(context.Background(), "id-1") - require.NoError(t, err) - - got.Traits = json.RawMessage(`{"bar":123}`) - _, err = pool.Update(context.Background(), got) - require.Error(t, err) - }) - - t.Run("case=updating credentials should work", func(t *testing.T) { - toUpdate, err := pool.Get(context.Background(), "id-1") - require.NoError(t, err) - - toUpdate.Credentials = map[CredentialsType]Credentials{ - CredentialsTypePassword: { - ID: CredentialsTypePassword, Identifiers: []string{"new-id-1", "new-id-2"}, - Config: json.RawMessage(`{}`), - }, - } - - _, err = pool.UpdateConfidential(context.Background(), toUpdate, toUpdate.Credentials) - require.NoError(t, err) - - got, err := pool.GetClassified(context.Background(), toUpdate.ID) - require.NoError(t, err) - - assert.Equal(t, []string{"new-id-1", "new-id-2"}, got.Credentials[CredentialsTypePassword].Identifiers) - }) - - t.Run("case=should fail to insert identity because credentials from traits exist", func(t *testing.T) { - i := newid("file://./stub/identity.schema.json", "should-not-matter") - i.Traits = json.RawMessage(`{"email":"id-2"}`) - _, err := pool.Create(context.Background(), i) - require.Error(t, err) - - i = newid("file://./stub/identity.schema.json", "id-4") - _, err = pool.Create(context.Background(), i) - require.NoError(t, err) - - i.Traits = json.RawMessage(`{"email":"id-2"}`) - _, err = pool.UpdateConfidential(context.Background(), i, i.Credentials) - require.Error(t, err) - }) - - t.Run("case=create and update an identity with credentials from traits", func(t *testing.T) { - i := newid("file://./stub/identity.schema.json", "id-3") - i.Traits = json.RawMessage(`{"email":"email-id-3"}`) - - _, err := pool.Create(context.Background(), i) - require.NoError(t, err) - - got, err := pool.GetClassified(context.Background(), i.ID) - require.NoError(t, err) - assert.Equal(t, []string{"email-id-3"}, got.Credentials[CredentialsTypePassword].Identifiers) - - i.Traits = json.RawMessage(`{"email":"email-id-4"}`) - _, err = pool.UpdateConfidential(context.Background(), i, i.Credentials) - require.NoError(t, err) - - got, err = pool.GetClassified(context.Background(), i.ID) - require.NoError(t, err) - assert.Equal(t, []string{"email-id-4"}, got.Credentials[CredentialsTypePassword].Identifiers) - }) - - t.Run("case=list", func(t *testing.T) { - is, err := pool.List(context.Background(), 10, 0) - require.NoError(t, err) - require.Len(t, is, 4) - assert.Equal(t, "id-1", is[0].ID) - assert.Equal(t, "id-2", is[1].ID) - }) - - t.Run("case=find identity by its credentials identifier", func(t *testing.T) { - expected := newid("file://./stub/identity.schema.json", "id-5") - expected.Traits = json.RawMessage(`{"email": "email-id-5"}`) - ct := expected.Credentials[CredentialsTypePassword] - ct.Identifiers = []string{"email-id-5"} - expected.Credentials[CredentialsTypePassword] = ct - - _, err := pool.Create(context.Background(), expected) - require.NoError(t, err) - - actual, creds, err := pool.FindByCredentialsIdentifier(context.Background(), CredentialsTypePassword, "email-id-5") - require.NoError(t, err) - - assert.EqualValues(t, ct, *creds) - - expected.Credentials = nil - assertEqual(t, expected, actual) - }) - - t.Run("case=delete an identity", func(t *testing.T) { - err := pool.Delete(context.Background(), "id-1") - require.NoError(t, err) - - _, err = pool.GetClassified(context.Background(), "id-1") - require.Error(t, err) - }) - - t.Run("case=create with empty credentials config", func(t *testing.T) { - // This test covers a case where the config value of a credentials setting is empty. This causes - // issues with postgres' json field. - i := newid("", "id-missing-creds-config") - i.SetCredentials(CredentialsTypePassword, Credentials{ - ID: CredentialsTypePassword, Identifiers: []string{"id-missing-creds-config"}, - Config: json.RawMessage(``), - }) - - _, err := pool.Create(context.Background(), i) - require.NoError(t, err) - }) - }) - } -} diff --git a/identity/validator.go b/identity/validator.go index ce2c936aac7..10539f313c2 100644 --- a/identity/validator.go +++ b/identity/validator.go @@ -1,9 +1,8 @@ package identity import ( - "github.com/pkg/errors" - "github.com/ory/gojsonschema" + "github.com/ory/x/errorsx" "github.com/ory/x/stringsx" "github.com/ory/kratos/driver/configuration" @@ -45,7 +44,7 @@ func (v *Validator) Validate(i *Identity) error { es..., ) - switch errs := errors.Cause(err).(type) { + switch errs := errorsx.Cause(err).(type) { case schema.ResultErrors: for k, err := range errs { errs[k].SetContext(schema.ContextSetRoot(schema.ContextRemoveRootStub(err.Context()), "traits")) diff --git a/identity/validator_test.go b/identity/validator_test.go index 843db0c33c5..1214fca4922 100644 --- a/identity/validator_test.go +++ b/identity/validator_test.go @@ -1,7 +1,6 @@ package identity_test import ( - "encoding/json" "fmt" "net/http" "net/http/httptest" @@ -51,7 +50,7 @@ func TestSchemaValidator(t *testing.T) { ts := httptest.NewServer(router) defer ts.Close() - conf, _ := internal.NewMemoryRegistry(t) + conf, _ := internal.NewRegistryDefault(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, ts.URL+"/schema/firstName") v := NewValidator(conf) @@ -61,45 +60,45 @@ func TestSchemaValidator(t *testing.T) { }{ { i: &Identity{ - Traits: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), }, }, { i: &Identity{ - Traits: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": -1 }`), + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": -1 }`), }, err: "must be greater than or equal to 1", }, { i: &Identity{ - Traits: json.RawMessage(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), }, err: "additional property whatever is not allowed", }, { i: &Identity{ TraitsSchemaURL: ts.URL + "/schema/whatever", - Traits: json.RawMessage(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "whatever": "first-name", "lastName": "last-name", "age": 1 }`), }, }, { i: &Identity{ TraitsSchemaURL: ts.URL + "/schema/whatever", - Traits: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), }, err: "additional property firstName is not allowed", }, { i: &Identity{ TraitsSchemaURL: ts.URL, - Traits: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), }, err: "An internal server error occurred, please contact the system administrator", }, { i: &Identity{ TraitsSchemaURL: "not-a-url", - Traits: json.RawMessage(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), + Traits: Traits(`{ "firstName": "first-name", "lastName": "last-name", "age": 1 }`), }, err: "An internal server error occurred, please contact the system administrator", }, diff --git a/internal/driver.go b/internal/driver.go index 801b96c002c..00874cfd0cb 100644 --- a/internal/driver.go +++ b/internal/driver.go @@ -1,25 +1,26 @@ package internal import ( + "context" + "os" + "path/filepath" "testing" - "github.com/jmoiron/sqlx" "github.com/stretchr/testify/require" - "github.com/ory/x/dbal" - "github.com/ory/viper" "github.com/ory/x/logrusx" "github.com/ory/kratos/driver" "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/x" ) func resetConfig() { viper.Set(configuration.ViperKeyDSN, nil) - viper.Set("LOG_LEVEL", "debug") + viper.Set("LOG_LEVEL", "trace") viper.Set(configuration.ViperKeyHasherArgon2ConfigMemory, 64) viper.Set(configuration.ViperKeyHasherArgon2ConfigIterations, 1) viper.Set(configuration.ViperKeyHasherArgon2ConfigParallelism, 1) @@ -33,22 +34,25 @@ func NewConfigurationWithDefaults() *configuration.ViperProvider { return configuration.NewViperProvider(logrusx.New()) } -func NewMemoryRegistry(t *testing.T) (*configuration.ViperProvider, *driver.RegistryMemory) { - conf := NewConfigurationWithDefaults() - viper.Set(configuration.ViperKeyDSN, "memory") +func NewRegistryDefault(t *testing.T) (*configuration.ViperProvider, *driver.RegistryDefault) { + conf, reg := NewRegistryDefaultWithDSN(t, "sqlite3://"+filepath.Join(os.TempDir(), x.NewUUID().String())+".sql?mode=memory&_fk=true") + require.NoError(t, reg.Persister().MigrateUp(context.Background())) + return conf, reg +} - reg, err := driver.NewRegistry(conf) +func NewRegistryDefaultWithDSN(t *testing.T, dsn string) (*configuration.ViperProvider, *driver.RegistryDefault) { + viper.Reset() + resetConfig() + + viper.Set(configuration.ViperKeyDSN, "sqlite3://"+filepath.Join(os.TempDir(), x.NewUUID().String())+".sql?mode=memory&_fk=true") + if dsn != "" { + viper.Set(configuration.ViperKeyDSN, dsn) + } + + d, err := driver.NewDefaultDriver(logrusx.New(), "test", "test", "test") require.NoError(t, err) - return conf, reg.WithConfig(conf).(*driver.RegistryMemory) -} -func NewRegistrySQL(t *testing.T, db *sqlx.DB) (*configuration.ViperProvider, *driver.RegistrySQL) { - viper.Set("LOG_LEVEL", "debug") - conf := NewConfigurationWithDefaults() - driver.SQLPurgeTestDatabase(t, db) - registry := driver.NewRegistrySQL().WithConfig(conf).(*driver.RegistrySQL).WithDB(db).(*driver.RegistrySQL) - count, err := registry.CreateSchemas(dbal.DriverPostgreSQL) require.NoError(t, err) - require.True(t, count > 0, "Applied %d migrations but expected more", count) - return conf, registry + require.NoError(t, d.Registry().Init()) + return d.Configuration().(*configuration.ViperProvider), d.Registry().(*driver.RegistryDefault) } diff --git a/internal/faker.go b/internal/faker.go index 19d788b416a..65da6c6dc57 100644 --- a/internal/faker.go +++ b/internal/faker.go @@ -14,6 +14,7 @@ import ( "github.com/ory/kratos/selfservice/flow/login" "github.com/ory/kratos/selfservice/flow/registration" "github.com/ory/kratos/selfservice/form" + "github.com/ory/kratos/x" ) func RegisterFakes() { @@ -55,40 +56,50 @@ func RegisterFakes() { } if err := faker.AddProvider("login_request_methods", func(v reflect.Value) (interface{}, error) { - methods := map[identity.CredentialsType]*login.RequestMethod{} - for i := 0; i <= rand.Intn(3); i++ { + var methods = make(map[identity.CredentialsType]*login.RequestMethod) + for _, ct := range []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeOIDC} { var f form.HTMLForm if err := faker.FakeData(&f); err != nil { return nil, err } - ct := identity.CredentialsType(randx.MustString(8, randx.AlphaLower)) methods[ct] = &login.RequestMethod{ Method: ct, - Config: &f, + Config: &login.RequestMethodConfig{RequestMethodConfigurator: &f}, } - } + } return methods, nil }); err != nil { panic(err) } if err := faker.AddProvider("registration_request_methods", func(v reflect.Value) (interface{}, error) { - methods := map[identity.CredentialsType]*registration.RequestMethod{} - for i := 0; i <= rand.Intn(3); i++ { + var methods = make(map[identity.CredentialsType]*registration.RequestMethod) + for _, ct := range []identity.CredentialsType{identity.CredentialsTypePassword, identity.CredentialsTypeOIDC} { var f form.HTMLForm if err := faker.FakeData(&f); err != nil { return nil, err } - ct := identity.CredentialsType(randx.MustString(8, randx.AlphaLower)) methods[ct] = ®istration.RequestMethod{ Method: ct, - Config: &f, + Config: ®istration.RequestMethodConfig{RequestMethodConfigurator: &f}, } } - return methods, nil }); err != nil { panic(err) } + + if err := faker.AddProvider("uuid", func(v reflect.Value) (interface{}, error) { + return x.NewUUID(), nil + }); err != nil { + panic(err) + } + + if err := faker.AddProvider("identity", func(v reflect.Value) (interface{}, error) { + var i identity.Identity + return &i, faker.FakeData(&i) + }); err != nil { + panic(err) + } } diff --git a/persistence/aliases/http_header.go b/persistence/aliases/http_header.go new file mode 100644 index 00000000000..2cb0c53c94b --- /dev/null +++ b/persistence/aliases/http_header.go @@ -0,0 +1,16 @@ +package aliases + +import ( + "database/sql/driver" + "net/http" +) + +type HTTPHeader http.Header + +func (h HTTPHeader) Scan(value interface{}) error { + return JSONScan(&h, value) +} + +func (h HTTPHeader) Value() (driver.Value, error) { + return JSONValue(&h) +} diff --git a/persistence/aliases/scanner_json.go b/persistence/aliases/scanner_json.go new file mode 100644 index 00000000000..219aba19b27 --- /dev/null +++ b/persistence/aliases/scanner_json.go @@ -0,0 +1,36 @@ +package aliases + +import ( + "bytes" + "database/sql/driver" + "encoding/json" + "fmt" + + "github.com/pkg/errors" +) + +func JSONScan(dst interface{}, value interface{}) error { + var b bytes.Buffer + switch v := value.(type) { + case []byte: + b.Write(v) + case string: + b.WriteString(v) + default: + return errors.Errorf("unable to decode value of type: %T %v", value, value) + } + + if err := json.NewDecoder(&b).Decode(&dst); err != nil { + return fmt.Errorf("unable to decode payload to: %s", err) + } + + return nil +} + +func JSONValue(src interface{}) (driver.Value, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(&src); err != nil { + return nil, err + } + return b.String(), nil +} diff --git a/persistence/reference.go b/persistence/reference.go new file mode 100644 index 00000000000..e433fe097e7 --- /dev/null +++ b/persistence/reference.go @@ -0,0 +1,31 @@ +package persistence + +import ( + "context" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/errorx" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/profile" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/session" +) + +type Provider interface { + Persister() Persister +} + +type Persister interface { + identity.Pool + registration.RequestPersister + login.RequestPersister + profile.RequestPersister + session.Persister + errorx.Persister + + Close(context.Context) error + Ping(context.Context) error + MigrationStatus(c context.Context) error + MigrateDown(c context.Context, steps int) error + MigrateUp(c context.Context) error +} diff --git a/persistence/sql/persister.go b/persistence/sql/persister.go new file mode 100644 index 00000000000..cc2c8ab623b --- /dev/null +++ b/persistence/sql/persister.go @@ -0,0 +1,76 @@ +package sql + +import ( + "context" + "time" + + "github.com/cenkalti/backoff" + "github.com/gobuffalo/packr" + "github.com/gobuffalo/pop" + "github.com/pkg/errors" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence" + "github.com/ory/kratos/x" +) + +var _ persistence.Persister = new(Persister) +var migrations = packr.NewBox("../../contrib/sql/migrations") + +type ( + persisterDependencies interface { + identity.ValidationProvider + x.LoggingProvider + } + Persister struct { + c *pop.Connection + mb pop.MigrationBox + r persisterDependencies + cf configuration.Provider + } +) + +func RetryConnect(dsn string) (c *pop.Connection, err error) { + bc := backoff.NewExponentialBackOff() + bc.MaxElapsedTime = time.Minute * 5 + bc.Reset() + + return c, backoff.Retry(func() (err error) { + c, err = pop.Connect(dsn) + return errors.WithStack(err) + }, bc) +} + +func NewPersister(r persisterDependencies, conf configuration.Provider, c *pop.Connection) (*Persister, error) { + m, err := pop.NewMigrationBox(migrations, c) + if err != nil { + return nil, errors.WithStack(err) + } + + return &Persister{c: c, mb: m, cf: conf, r: r}, nil +} + +func (p *Persister) MigrationStatus(c context.Context) error { + return errors.WithStack(p.mb.Status()) +} + +func (p *Persister) MigrateDown(c context.Context, steps int) error { + return errors.WithStack(p.mb.Down(steps)) +} + +func (p *Persister) MigrateUp(c context.Context) error { + return errors.WithStack(p.mb.Up()) +} + +func (p *Persister) Close(c context.Context) error { + return errors.WithStack(p.c.Close()) +} + +func (p *Persister) Ping(c context.Context) error { + type pinger interface { + Ping() error + } + + return errors.WithStack(p.c.Store.(pinger).Ping()) +} diff --git a/persistence/sql/persister_errorx.go b/persistence/sql/persister_errorx.go new file mode 100644 index 00000000000..1e45f4b3eba --- /dev/null +++ b/persistence/sql/persister_errorx.go @@ -0,0 +1,118 @@ +package sql + +import ( + "bytes" + "context" + "encoding/json" + stderr "errors" + "time" + + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/errorsx" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/schema" + "github.com/ory/kratos/selfservice/errorx" +) + +var _ errorx.Persister = new(Persister) + +type errorxContainer struct { + ID uuid.UUID `json:"-" db:"id"` + Errors json.RawMessage `json:"-" db:"errors"` + + SeenAt time.Time `json:"-" db:"seen_at"` + WasSeen bool `json:"-" db:"was_seen"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` +} + +func (e errorxContainer) TableName() string { + return "selfservice_errors" +} + +func (p *Persister) Add(ctx context.Context, errs ...error) (uuid.UUID, error) { + buf, err := p.encodeSelfServiceErrors(errs) + if err != nil { + return uuid.Nil, err + } + + c := &errorxContainer{ + Errors: buf.Bytes(), + WasSeen: false, + } + + if err := p.c.Create(c); err != nil { + return uuid.Nil, sqlcon.HandleError(err) + } + + return c.ID, nil +} + +func (p *Persister) Read(ctx context.Context, id uuid.UUID) ([]json.RawMessage, error) { + var errs []json.RawMessage + + var c errorxContainer + if err := p.c.Find(&c, id); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := json.NewDecoder(bytes.NewBuffer(c.Errors)).Decode(&errs); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to decode stored error messages from SQL datastore.").WithDebug(err.Error())) + } + + if err := p.c.RawQuery("UPDATE selfservice_errors SET was_seen = true, seen_at = ? WHERE id = ?", time.Now().UTC(), id).Exec(); err != nil { + return nil, sqlcon.HandleError(err) + } + + return errs, nil +} + +func (p *Persister) Clear(ctx context.Context, olderThan time.Duration, force bool) (err error) { + if force { + err = p.c.RawQuery("DELETE FROM selfservice_errors WHERE seen_at < ?", olderThan).Exec() + } else { + err = p.c.RawQuery("DELETE FROM selfservice_errors WHERE was_seen=true AND seen_at < ?", time.Now().UTC().Add(-olderThan)).Exec() + } + + return sqlcon.HandleError(err) +} + +func (p *Persister) encodeSelfServiceErrors(errs []error) (*bytes.Buffer, error) { + es := make([]interface{}, len(errs)) + for k, e := range errs { + e = errorsx.Cause(e) + if u := stderr.Unwrap(e); u != nil { + e = u + } + + if e == nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug.")) + } + + // Convert to a default error if the error type is unknown. Helps to properly + // pass through system errors. + switch e.(type) { + case *herodot.DefaultError: + case *schema.ResultErrors: + case schema.ResultErrors: + default: + e = herodot.ToDefaultError(e, "") + } + + es[k] = e + } + + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(es); err != nil { + return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error())) + } + + return &b, nil +} diff --git a/persistence/sql/persister_identity.go b/persistence/sql/persister_identity.go new file mode 100644 index 00000000000..b8a1d653b5e --- /dev/null +++ b/persistence/sql/persister_identity.go @@ -0,0 +1,255 @@ +package sql + +import ( + "context" + "database/sql" + "encoding/json" + "strings" + + "github.com/gobuffalo/pop" + "github.com/gofrs/uuid" + "github.com/pkg/errors" + + "github.com/ory/herodot" + "github.com/ory/x/errorsx" + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" +) + +var _ identity.Pool = new(Persister) + +func (p *Persister) FindByCredentialsIdentifier(ctx context.Context, ct identity.CredentialsType, match string) (*identity.Identity, *identity.Credentials, error) { + var cts []identity.CredentialsTypeTable + if err := p.c.All(&cts); err != nil { + return nil, nil, sqlcon.HandleError(err) + } + + var find struct { + IdentityID uuid.UUID `db:"identity_id"` + } + + if err := p.c.RawQuery(`SELECT + ic.identity_id +FROM identity_credentials ic + INNER JOIN identity_credential_types ict on ic.identity_credential_type_id = ict.id + INNER JOIN identity_credential_identifiers ici on ic.id = ici.identity_credential_id +WHERE ici.identifier = ? + AND ict.name = ?`, match, ct).First(&find); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + return nil, nil, herodot.ErrNotFound.WithTrace(err).WithReasonf(`No identity matching credentials identifier "%s" could be found.`, match) + } + + return nil, nil, sqlcon.HandleError(err) + } + + i, err := p.GetIdentityConfidential(ctx, find.IdentityID) + if err != nil { + return nil, nil, err + } + + creds, ok := i.GetCredentials(ct) + if !ok { + return nil, nil, errors.WithStack(herodot.ErrInternalServerError.WithReasonf("The SQL adapter failed to return the appropriate credentials_type \"%s\". This is a bug in the code.", ct)) + } + + return i.CopyWithoutCredentials(), creds, nil +} + +func findOrCreateIdentityCredentialsType(_ context.Context, tx *pop.Connection, ct identity.CredentialsType) (*identity.CredentialsTypeTable, error) { + var m identity.CredentialsTypeTable + if err := tx.Where("name = ?", ct).First(&m); err != nil { + if errors.Cause(err) == sql.ErrNoRows { + m.Name = ct + if err := sqlcon.HandleError(tx.Create(&m)); err != nil { + return nil, err + } + return &m, nil + } + return nil, sqlcon.HandleError(err) + } + + return &m, nil +} + +func createIdentityCredentials(ctx context.Context, tx *pop.Connection, i *identity.Identity) error { + for k, cred := range i.Credentials { + cred.IdentityID = i.ID + if len(cred.Config) == 0 { + cred.Config = json.RawMessage("{}") + } + + ct, err := findOrCreateIdentityCredentialsType(ctx, tx, cred.Type) + if err != nil { + return err + } + + cred.CredentialTypeID = ct.ID + if err := tx.Create(&cred); err != nil { + return err + } + + for _, ids := range cred.Identifiers { + if strings.Contains(ids, "@") && cred.Type == identity.CredentialsTypePassword { + ids = strings.ToLower(ids) + } + + ci := &identity.CredentialIdentifier{ + Identifier: ids, + IdentityCredentialsID: cred.ID, + } + if err := tx.Create(ci); err != nil { + return err + } + } + + i.Credentials[k] = cred + } + + return nil +} + +func (p *Persister) CreateIdentity(ctx context.Context, i *identity.Identity) error { + if i.TraitsSchemaURL == "" { + i.TraitsSchemaURL = p.cf.DefaultIdentityTraitsSchemaURL().String() + } + + if len(i.Traits) == 0 { + i.Traits = identity.Traits("{}") + } + + if err := p.validateIdentity(i); err != nil { + return err + } + + return sqlcon.HandleError(p.c.Transaction(func(tx *pop.Connection) error { + if err := tx.Create(i); err != nil { + return err + } + + return createIdentityCredentials(ctx, tx, i) + })) +} + +func (p *Persister) ListIdentities(ctx context.Context, limit, offset int) ([]identity.Identity, error) { + is := make([]identity.Identity, 0) + return is, sqlcon.HandleError(p.c.RawQuery("SELECT * FROM identities LIMIT ? OFFSET ?", limit, offset).All(&is)) +} + +func (p *Persister) UpdateIdentityConfidential(ctx context.Context, i *identity.Identity) error { + if err := p.validateIdentity(i); err != nil { + return err + } + + return sqlcon.HandleError(p.c.Transaction(func(tx *pop.Connection) error { + if err := tx.RawQuery(`DELETE FROM identity_credentials WHERE identity_id = ?`, i.ID).Exec(); err != nil { + return err + } + + if err := tx.Update(i); err != nil { + return err + } + + return createIdentityCredentials(ctx, tx, i) + })) +} + +func (p *Persister) UpdateIdentity(ctx context.Context, i *identity.Identity) error { + if err := p.validateIdentity(i); err != nil { + return err + } + + fs, err := p.GetIdentityConfidential(ctx, i.ID) + if err != nil { + return err + } + + // If credential identifiers have changed we need to block this action UNLESS + // the identity has been authenticated in that request: + // + // - https://security.stackexchange.com/questions/24291/why-do-we-ask-for-a-users-existing-password-when-changing-their-password + if !i.CredentialsEqual(fs.Credentials) { + + // !! WARNING !! + // + // This will leak the credential options which may include the hashed password. Do not use seriously: + // + // p.r.Logger(). + // WithField("original_credentials", fmt.Sprintf("%+v", fs.Credentials)). + // WithField("updated_credentials", fmt.Sprintf("%+v", i.Credentials)). + // Trace("Credentials changed unexpectedly in UpdateIdentity.") + + return errors.WithStack( + herodot.ErrInternalServerError. + WithReasonf(`A field was modified that updates one or more credentials-related settings. This action was blocked because a unprivileged DBAL method was used to execute the update. This is either a configuration issue, or a bug.`)) + } + + return sqlcon.HandleError(p.c.Update(i)) +} + +func (p *Persister) DeleteIdentity(_ context.Context, id uuid.UUID) error { + count, err := p.c.RawQuery("DELETE FROM identities WHERE id = ?", id).ExecWithCount() + if err != nil { + return sqlcon.HandleError(err) + } + if count == 0 { + return sqlcon.ErrNoRows + } + return nil +} + +func (p *Persister) GetIdentity(_ context.Context, id uuid.UUID) (*identity.Identity, error) { + var i identity.Identity + if err := p.c.Find(&i, id); err != nil { + return nil, sqlcon.HandleError(err) + } + i.Credentials = nil + return &i, nil +} + +func (p *Persister) GetIdentityConfidential(_ context.Context, id uuid.UUID) (*identity.Identity, error) { + var i identity.Identity + if err := p.c.Eager().Find(&i, id); err != nil { + return nil, sqlcon.HandleError(err) + } + + var cts []identity.CredentialsTypeTable + if err := p.c.All(&cts); err != nil { + return nil, sqlcon.HandleError(err) + } + + i.Credentials = map[identity.CredentialsType]identity.Credentials{} + for _, creds := range i.CredentialsCollection { + var cs identity.CredentialIdentifierCollection + if err := p.c.Where("identity_credential_id = ?", creds.ID).All(&cs); err != nil { + return nil, sqlcon.HandleError(err) + } + + creds.CredentialIdentifierCollection = nil + creds.Identifiers = make([]string, len(cs)) + for k := range cs { + for _, ct := range cts { + if ct.ID == creds.CredentialTypeID { + creds.Type = ct.Name + } + } + creds.Identifiers[k] = cs[k].Identifier + } + i.Credentials[creds.Type] = creds + } + i.CredentialsCollection = nil + + return &i, nil +} + +func (p *Persister) validateIdentity(i *identity.Identity) error { + if err := p.r.IdentityValidator().Validate(i); err != nil { + if _, ok := errorsx.Cause(err).(schema.ResultErrors); ok { + return errors.WithStack(herodot.ErrBadRequest.WithReasonf("%s", err)) + } + return err + } + + return nil +} diff --git a/persistence/sql/persister_login.go b/persistence/sql/persister_login.go new file mode 100644 index 00000000000..b78eef04ddc --- /dev/null +++ b/persistence/sql/persister_login.go @@ -0,0 +1,48 @@ +package sql + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/login" +) + +var _ login.RequestPersister = new(Persister) + +func (p *Persister) CreateLoginRequest(_ context.Context, r *login.Request) error { + return p.c.Eager().Create(r) +} + +func (p *Persister) GetLoginRequest(_ context.Context, id uuid.UUID) (*login.Request, error) { + var r login.Request + if err := p.c.Eager().Find(&r, id); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := (&r).AfterFind(p.c); err != nil { + return nil, err + } + + return &r, nil +} + +func (p *Persister) UpdateLoginRequest(ctx context.Context, id uuid.UUID, ct identity.CredentialsType, rm *login.RequestMethod) error { + rr, err := p.GetLoginRequest(ctx, id) + if err != nil { + return err + } + + method, ok := rr.Methods[ct] + if !ok { + rm.RequestID = rr.ID + rm.Method = ct + return p.c.Save(rm) + } + + method.Config = rm.Config + return p.c.Save(method) +} diff --git a/persistence/sql/persister_profile.go b/persistence/sql/persister_profile.go new file mode 100644 index 00000000000..0ac1f751d7e --- /dev/null +++ b/persistence/sql/persister_profile.go @@ -0,0 +1,30 @@ +package sql + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/selfservice/flow/profile" +) + +var _ profile.RequestPersister = new(Persister) + +func (p *Persister) CreateProfileRequest(_ context.Context, r *profile.Request) error { + r.IdentityID = r.Identity.ID + return sqlcon.HandleError(p.c.Create(r)) // This must not be eager or identities will be created / updated +} + +func (p *Persister) GetProfileRequest(_ context.Context, id uuid.UUID) (*profile.Request, error) { + var r profile.Request + if err := p.c.Eager().Find(&r, id); err != nil { + return nil, sqlcon.HandleError(err) + } + return &r, nil +} + +func (p *Persister) UpdateProfileRequest(ctx context.Context, r *profile.Request) error { + return sqlcon.HandleError(p.c.Update(r)) // This must not be eager or identities will be created / updated +} diff --git a/persistence/sql/persister_registration.go b/persistence/sql/persister_registration.go new file mode 100644 index 00000000000..92b495fdc2f --- /dev/null +++ b/persistence/sql/persister_registration.go @@ -0,0 +1,46 @@ +package sql + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/flow/registration" +) + +func (p *Persister) CreateRegistrationRequest(_ context.Context, r *registration.Request) error { + return p.c.Eager().Create(r) +} + +func (p *Persister) GetRegistrationRequest(_ context.Context, id uuid.UUID) (*registration.Request, error) { + var r registration.Request + if err := p.c.Eager().Find(&r, id); err != nil { + return nil, sqlcon.HandleError(err) + } + + if err := (&r).AfterFind(p.c); err != nil { + return nil, err + } + + return &r, nil +} + +func (p *Persister) UpdateRegistrationRequest(ctx context.Context, id uuid.UUID, ct identity.CredentialsType, rm *registration.RequestMethod) error { + rr, err := p.GetRegistrationRequest(ctx, id) + if err != nil { + return err + } + + method, ok := rr.Methods[ct] + if !ok { + rm.RequestID = rr.ID + rm.Method = ct + return p.c.Save(rm) + } + + method.Config = rm.Config + return p.c.Save(method) +} diff --git a/persistence/sql/persister_session.go b/persistence/sql/persister_session.go new file mode 100644 index 00000000000..1eb0ce09bf8 --- /dev/null +++ b/persistence/sql/persister_session.go @@ -0,0 +1,29 @@ +package sql + +import ( + "context" + + "github.com/gofrs/uuid" + + "github.com/ory/x/sqlcon" + + "github.com/ory/kratos/session" +) + +var _ session.Persister = new(Persister) + +func (p *Persister) GetSession(ctx context.Context, sid uuid.UUID) (*session.Session, error) { + var s session.Session + if err := p.c.Eager().Find(&s, sid); err != nil { + return nil, sqlcon.HandleError(err) + } + return &s, nil +} + +func (p *Persister) CreateSession(ctx context.Context, s *session.Session) error { + return p.c.Create(s) // This must not be eager or identities will be created / updated +} + +func (p *Persister) DeleteSession(ctx context.Context, sid uuid.UUID) error { + return p.c.Destroy(&session.Session{ID: sid}) // This must not be eager or identities will be created / updated +} diff --git a/persistence/sql/persister_test.go b/persistence/sql/persister_test.go new file mode 100644 index 00000000000..fd57b6068e1 --- /dev/null +++ b/persistence/sql/persister_test.go @@ -0,0 +1,138 @@ +package sql_test + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "github.com/gobuffalo/pop" + "github.com/gobuffalo/pop/logging" + "github.com/google/uuid" + + "github.com/ory/x/sqlcon/dockertest" + + // "github.com/ory/x/sqlcon/dockertest" + "github.com/stretchr/testify/require" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/internal" + "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/selfservice/flow/profile" + "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/session" +) + +// Workaround for https://github.com/gobuffalo/pop/pull/481 +var sqlite = fmt.Sprintf("sqlite3://%s.sqlite?_fk=true&mode=rwc", filepath.Join(os.TempDir(), uuid.New().String())) + +func init() { + internal.RegisterFakes() + // pop.Debug = true +} + +// nolint:staticcheck +func TestMain(m *testing.M) { + atexit := dockertest.NewOnExit() + atexit.Add(func() { + _ = os.Remove(strings.TrimPrefix(sqlite, "sqlite://")) + dockertest.KillAllTestDatabases() + }) + atexit.Exit(m.Run()) +} + +func pl(t *testing.T) func(lvl logging.Level, s string, args ...interface{}) { + return func(lvl logging.Level, s string, args ...interface{}) { + if pop.Debug == false { + return + } + + if lvl == logging.SQL { + if len(args) > 0 { + xargs := make([]string, len(args)) + for i, a := range args { + switch a.(type) { + case string: + xargs[i] = fmt.Sprintf("%q", a) + default: + xargs[i] = fmt.Sprintf("%v", a) + } + } + s = fmt.Sprintf("%s - %s | %s", lvl, s, xargs) + } else { + s = fmt.Sprintf("%s - %s", lvl, s) + } + } else { + s = fmt.Sprintf(s, args...) + s = fmt.Sprintf("%s - %s", lvl, s) + } + t.Log(s) + } +} + +func TestPersister(t *testing.T) { + conns := map[string]string{ + "sqlite": sqlite, + } + + var l sync.Mutex + if !testing.Short() { + funcs := map[string]func(t *testing.T) string{ + "mysql": dockertest.RunTestMySQL, + "postgres": dockertest.RunTestPostgreSQL, + // "cockroach": dockertest.RunTestCockroachDB, // pending: https://github.com/gobuffalo/fizz/pull/69 + } + + var wg sync.WaitGroup + wg.Add(len(funcs)) + + for k, f := range funcs { + go func(s string, f func(t *testing.T) string) { + defer wg.Done() + db := f(t) + l.Lock() + conns[s] = db + l.Unlock() + }(k, f) + } + + wg.Wait() + } + + for name, dsn := range conns { + t.Run("database="+name, func(t *testing.T) { + _, reg := internal.NewRegistryDefaultWithDSN(t, dsn) + p := reg.Persister() + + // pop.SetLogger(pl(t)) + require.NoError(t, p.MigrationStatus(context.Background())) + require.NoError(t, p.MigrateUp(context.Background())) + + t.Run("contract=identity.TestPool", func(t *testing.T) { + pop.SetLogger(pl(t)) + identity.TestPool(p)(t) + }) + t.Run("contract=registration.TestRequestPersister", func(t *testing.T) { + pop.SetLogger(pl(t)) + registration.TestRequestPersister(p)(t) + }) + t.Run("contract=login.TestRequestPersister", func(t *testing.T) { + pop.SetLogger(pl(t)) + login.TestRequestPersister(p)(t) + }) + t.Run("contract=profile.TestRequestPersister", func(t *testing.T) { + pop.SetLogger(pl(t)) + profile.TestRequestPersister(p)(t) + }) + t.Run("contract=session.TestRequestPersister", func(t *testing.T) { + pop.SetLogger(pl(t)) + session.TestPersister(p)(t) + }) + }) + t.Logf("DSN: %s", dsn) + } + +} diff --git a/persistence/sql/stub/identity-2.schema.json b/persistence/sql/stub/identity-2.schema.json new file mode 100644 index 00000000000..ec435cd1a95 --- /dev/null +++ b/persistence/sql/stub/identity-2.schema.json @@ -0,0 +1,11 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "bar": { + "type": "string" + } + } +} diff --git a/persistence/sql/stub/identity.schema.json b/persistence/sql/stub/identity.schema.json new file mode 100644 index 00000000000..fe94126c952 --- /dev/null +++ b/persistence/sql/stub/identity.schema.json @@ -0,0 +1,21 @@ +{ + "$id": "https://example.com/registration.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "bar": { + "type": "string" + }, + "email": { + "type": "string", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + } +} diff --git a/sdk/go/kratos/models/identity.go b/sdk/go/kratos/models/identity.go index b9f5cb387a4..09dce7e7043 100644 --- a/sdk/go/kratos/models/identity.go +++ b/sdk/go/kratos/models/identity.go @@ -13,28 +13,18 @@ import ( "github.com/go-openapi/validate" ) -// Identity Identity represents an ORY Kratos identity -// -// An identity can be a real human, a service, an IoT device - everything that -// can be described as an "actor" in a system. -// swagger:model identity +// Identity identity +// swagger:model Identity type Identity struct { - // Credentials represents all credentials that can be used for authenticating this identity. - Credentials map[string]IdentityCredentials `json:"credentials,omitempty"` - - // ID is a unique identifier chosen by you. It can be a URN (e.g. "arn:aws:iam::123456789012"), - // a stringified integer (e.g. "123456789012"), a uuid (e.g. "9f425a8d-7efc-4768-8f23-7647a74fdf13"). It is up to you - // to pick a format you'd like. It is discouraged to use a personally identifiable value here, like the username - // or the email, as this field is immutable. + // id // Required: true - ID *string `json:"id"` + // Format: uuid4 + ID UUID `json:"id"` - // Traits represent an identity's traits. The identity is able to create, modify, and delete traits - // in a self-service manner. The input will always be validated against the JSON Schema defined - // in `traits_schema_url`. + // traits // Required: true - Traits interface{} `json:"traits"` + Traits Traits `json:"traits"` // TraitsSchemaURL is the JSON Schema to be used for validating the identity's traits. // @@ -46,10 +36,6 @@ type Identity struct { func (m *Identity) Validate(formats strfmt.Registry) error { var res []error - if err := m.validateCredentials(formats); err != nil { - res = append(res, err) - } - if err := m.validateID(formats); err != nil { res = append(res, err) } @@ -64,31 +50,12 @@ func (m *Identity) Validate(formats strfmt.Registry) error { return nil } -func (m *Identity) validateCredentials(formats strfmt.Registry) error { - - if swag.IsZero(m.Credentials) { // not required - return nil - } - - for k := range m.Credentials { - - if err := validate.Required("credentials"+"."+k, "body", m.Credentials[k]); err != nil { - return err - } - if val, ok := m.Credentials[k]; ok { - if err := val.Validate(formats); err != nil { - return err - } - } - - } - - return nil -} - func (m *Identity) validateID(formats strfmt.Registry) error { - if err := validate.Required("id", "body", m.ID); err != nil { + if err := m.ID.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("id") + } return err } diff --git a/sdk/go/kratos/models/identity_credentials.go b/sdk/go/kratos/models/identity_credentials.go deleted file mode 100644 index 2df940ceb8d..00000000000 --- a/sdk/go/kratos/models/identity_credentials.go +++ /dev/null @@ -1,79 +0,0 @@ -// Code generated by go-swagger; DO NOT EDIT. - -package models - -// This file was generated by the swagger tool. -// Editing this file might prove futile when you re-run the swagger generate command - -import ( - strfmt "github.com/go-openapi/strfmt" - - "github.com/go-openapi/errors" - "github.com/go-openapi/swag" -) - -// IdentityCredentials Credentials represents a specific credential type -// swagger:model identityCredentials -type IdentityCredentials struct { - - // Config contains the concrete credential payload. This might contain the bcrypt-hashed password, or the email - // for passwordless authentication. - // - // type: string - // format: binary - Config interface{} `json:"config,omitempty"` - - // id - ID CredentialsType `json:"id,omitempty"` - - // Identifiers represents a list of unique identifiers this credential type matches. - Identifiers []string `json:"identifiers"` -} - -// Validate validates this identity credentials -func (m *IdentityCredentials) Validate(formats strfmt.Registry) error { - var res []error - - if err := m.validateID(formats); err != nil { - res = append(res, err) - } - - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } - return nil -} - -func (m *IdentityCredentials) validateID(formats strfmt.Registry) error { - - if swag.IsZero(m.ID) { // not required - return nil - } - - if err := m.ID.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("id") - } - return err - } - - return nil -} - -// MarshalBinary interface implementation -func (m *IdentityCredentials) MarshalBinary() ([]byte, error) { - if m == nil { - return nil, nil - } - return swag.WriteJSON(m) -} - -// UnmarshalBinary interface implementation -func (m *IdentityCredentials) UnmarshalBinary(b []byte) error { - var res IdentityCredentials - if err := swag.ReadJSON(b, &res); err != nil { - return err - } - *m = res - return nil -} diff --git a/sdk/go/kratos/models/login_request.go b/sdk/go/kratos/models/login_request.go index d974a365292..70de61bff63 100644 --- a/sdk/go/kratos/models/login_request.go +++ b/sdk/go/kratos/models/login_request.go @@ -25,9 +25,9 @@ type LoginRequest struct { // Format: date-time ExpiresAt strfmt.DateTime `json:"expires_at,omitempty"` - // ID represents the request's unique ID. When performing the login flow, this - // represents the id in the login ui's query parameter: http:///?request= - ID string `json:"id,omitempty"` + // id + // Format: uuid4 + ID UUID `json:"id,omitempty"` // IssuedAt is the time (UTC) when the request occurred. // Format: date-time @@ -54,6 +54,10 @@ func (m *LoginRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + if err := m.validateIssuedAt(formats); err != nil { res = append(res, err) } @@ -97,6 +101,22 @@ func (m *LoginRequest) validateExpiresAt(formats strfmt.Registry) error { return nil } +func (m *LoginRequest) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID) { // not required + return nil + } + + if err := m.ID.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("id") + } + return err + } + + return nil +} + func (m *LoginRequest) validateIssuedAt(formats strfmt.Registry) error { if swag.IsZero(m.IssuedAt) { // not required diff --git a/sdk/go/kratos/models/login_request_method.go b/sdk/go/kratos/models/login_request_method.go index c9938f84d55..ff13f554ee1 100644 --- a/sdk/go/kratos/models/login_request_method.go +++ b/sdk/go/kratos/models/login_request_method.go @@ -27,10 +27,6 @@ type LoginRequestMethod struct { func (m *LoginRequestMethod) Validate(formats strfmt.Registry) error { var res []error - if err := m.validateConfig(formats); err != nil { - res = append(res, err) - } - if err := m.validateMethod(formats); err != nil { res = append(res, err) } @@ -41,24 +37,6 @@ func (m *LoginRequestMethod) Validate(formats strfmt.Registry) error { return nil } -func (m *LoginRequestMethod) validateConfig(formats strfmt.Registry) error { - - if swag.IsZero(m.Config) { // not required - return nil - } - - if m.Config != nil { - if err := m.Config.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("config") - } - return err - } - } - - return nil -} - func (m *LoginRequestMethod) validateMethod(formats strfmt.Registry) error { if swag.IsZero(m.Method) { // not required diff --git a/sdk/go/kratos/models/profile_management_request.go b/sdk/go/kratos/models/profile_management_request.go index 518b58df860..816c8afb917 100644 --- a/sdk/go/kratos/models/profile_management_request.go +++ b/sdk/go/kratos/models/profile_management_request.go @@ -30,9 +30,9 @@ type ProfileManagementRequest struct { // form Form *Form `json:"form,omitempty"` - // ID represents the request's unique ID. When performing the profile management flow, this - // represents the id in the profile ui's query parameter: http://?request= - ID string `json:"id,omitempty"` + // id + // Format: uuid4 + ID UUID `json:"id,omitempty"` // identity Identity *Identity `json:"identity,omitempty"` @@ -63,6 +63,10 @@ func (m *ProfileManagementRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + if err := m.validateIdentity(formats); err != nil { res = append(res, err) } @@ -108,6 +112,22 @@ func (m *ProfileManagementRequest) validateForm(formats strfmt.Registry) error { return nil } +func (m *ProfileManagementRequest) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID) { // not required + return nil + } + + if err := m.ID.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("id") + } + return err + } + + return nil +} + func (m *ProfileManagementRequest) validateIdentity(formats strfmt.Registry) error { if swag.IsZero(m.Identity) { // not required diff --git a/sdk/go/kratos/models/registration_request.go b/sdk/go/kratos/models/registration_request.go index c927b412af9..c360d5ec199 100644 --- a/sdk/go/kratos/models/registration_request.go +++ b/sdk/go/kratos/models/registration_request.go @@ -25,9 +25,9 @@ type RegistrationRequest struct { // Format: date-time ExpiresAt strfmt.DateTime `json:"expires_at,omitempty"` - // ID represents the request's unique ID. When performing the registration flow, this - // represents the id in the registration ui's query parameter: http://registration-ui/?request= - ID string `json:"id,omitempty"` + // id + // Format: uuid4 + ID UUID `json:"id,omitempty"` // IssuedAt is the time (UTC) when the request occurred. // Format: date-time @@ -54,6 +54,10 @@ func (m *RegistrationRequest) Validate(formats strfmt.Registry) error { res = append(res, err) } + if err := m.validateID(formats); err != nil { + res = append(res, err) + } + if err := m.validateIssuedAt(formats); err != nil { res = append(res, err) } @@ -97,6 +101,22 @@ func (m *RegistrationRequest) validateExpiresAt(formats strfmt.Registry) error { return nil } +func (m *RegistrationRequest) validateID(formats strfmt.Registry) error { + + if swag.IsZero(m.ID) { // not required + return nil + } + + if err := m.ID.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("id") + } + return err + } + + return nil +} + func (m *RegistrationRequest) validateIssuedAt(formats strfmt.Registry) error { if swag.IsZero(m.IssuedAt) { // not required diff --git a/sdk/go/kratos/models/registration_request_method.go b/sdk/go/kratos/models/registration_request_method.go index f4c89c6ff76..9a563398e79 100644 --- a/sdk/go/kratos/models/registration_request_method.go +++ b/sdk/go/kratos/models/registration_request_method.go @@ -27,10 +27,6 @@ type RegistrationRequestMethod struct { func (m *RegistrationRequestMethod) Validate(formats strfmt.Registry) error { var res []error - if err := m.validateConfig(formats); err != nil { - res = append(res, err) - } - if err := m.validateMethod(formats); err != nil { res = append(res, err) } @@ -41,24 +37,6 @@ func (m *RegistrationRequestMethod) Validate(formats strfmt.Registry) error { return nil } -func (m *RegistrationRequestMethod) validateConfig(formats strfmt.Registry) error { - - if swag.IsZero(m.Config) { // not required - return nil - } - - if m.Config != nil { - if err := m.Config.Validate(formats); err != nil { - if ve, ok := err.(*errors.Validation); ok { - return ve.ValidateName("config") - } - return err - } - } - - return nil -} - func (m *RegistrationRequestMethod) validateMethod(formats strfmt.Registry) error { if swag.IsZero(m.Method) { // not required diff --git a/sdk/go/kratos/models/request_method_configurator.go b/sdk/go/kratos/models/request_method_configurator.go new file mode 100644 index 00000000000..687449372af --- /dev/null +++ b/sdk/go/kratos/models/request_method_configurator.go @@ -0,0 +1,10 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +// RequestMethodConfigurator request method configurator +// swagger:model RequestMethodConfigurator +type RequestMethodConfigurator interface{} diff --git a/sdk/go/kratos/models/c_s_r_f_setter.go b/sdk/go/kratos/models/traits.go similarity index 69% rename from sdk/go/kratos/models/c_s_r_f_setter.go rename to sdk/go/kratos/models/traits.go index 9f48befa1a0..e39059ab688 100644 --- a/sdk/go/kratos/models/c_s_r_f_setter.go +++ b/sdk/go/kratos/models/traits.go @@ -5,6 +5,6 @@ package models // This file was generated by the swagger tool. // Editing this file might prove futile when you re-run the swagger generate command -// CSRFSetter c s r f setter -// swagger:model CSRFSetter -type CSRFSetter interface{} +// Traits traits +// swagger:model Traits +type Traits interface{} diff --git a/sdk/go/kratos/models/uuid.go b/sdk/go/kratos/models/uuid.go new file mode 100644 index 00000000000..0a86f483b5f --- /dev/null +++ b/sdk/go/kratos/models/uuid.go @@ -0,0 +1,31 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + strfmt "github.com/go-openapi/strfmt" + + "github.com/go-openapi/errors" + "github.com/go-openapi/validate" +) + +// UUID UUID +// swagger:model UUID +type UUID strfmt.UUID4 + +// Validate validates this UUID +func (m UUID) Validate(formats strfmt.Registry) error { + var res []error + + if err := validate.FormatOf("", "body", "uuid4", strfmt.UUID4(m).String(), formats); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/selfservice/errorx/handler.go b/selfservice/errorx/handler.go index b466f4dc37d..493dcbe327d 100644 --- a/selfservice/errorx/handler.go +++ b/selfservice/errorx/handler.go @@ -10,8 +10,8 @@ import ( type ( handlerDependencies interface { - ErrorManager() Manager x.WriterProvider + PersistenceProvider } HandlerProvider interface { SelfServiceErrorHandler() *Handler @@ -32,7 +32,7 @@ func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) { } func (h *Handler) get(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - es, err := h.r.ErrorManager().Read(r.Context(), r.URL.Query().Get("error")) + es, err := h.r.SelfServiceErrorPersister().Read(r.Context(), x.ParseUUID(r.URL.Query().Get("error"))) if err != nil { h.r.Writer().WriteError(w, r, err) return diff --git a/selfservice/errorx/handler_test.go b/selfservice/errorx/handler_test.go index a58d4fe32fc..c74609fcdce 100644 --- a/selfservice/errorx/handler_test.go +++ b/selfservice/errorx/handler_test.go @@ -13,6 +13,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ory/x/errorsx" + "github.com/ory/herodot" "github.com/ory/kratos/internal" @@ -21,7 +23,7 @@ import ( ) func TestHandler(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) h := errorx.NewHandler(reg) router := x.NewRouterPublic() @@ -55,10 +57,10 @@ func TestHandler(t *testing.T) { }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - id, err := reg.ErrorManager().Add(context.Background(), tc.gave...) + id, err := reg.SelfServiceErrorPersister().Add(context.Background(), tc.gave...) require.NoError(t, err) - res, err := ts.Client().Get(ts.URL + "/errors?error=" + id) + res, err := ts.Client().Get(ts.URL + "/errors?error=" + id.String()) require.NoError(t, err) defer res.Body.Close() assert.EqualValues(t, http.StatusOK, res.StatusCode) @@ -68,7 +70,7 @@ func TestHandler(t *testing.T) { gg := make([]error, len(tc.gave)) for k, g := range tc.gave { - gg[k] = errors.Cause(g) + gg[k] = errorsx.Cause(g) } expected, err := json.Marshal(gg) diff --git a/selfservice/errorx/helper.go b/selfservice/errorx/helper.go index eb38aa9c906..6eb8acf5600 100644 --- a/selfservice/errorx/helper.go +++ b/selfservice/errorx/helper.go @@ -9,13 +9,15 @@ import ( "github.com/stretchr/testify/require" "github.com/ory/herodot" + + "github.com/ory/kratos/x" ) -func NewErrorTestServer(t *testing.T, reg ManagementProvider) *httptest.Server { +func NewErrorTestServer(t *testing.T, reg PersistenceProvider) *httptest.Server { logger := logrus.New() writer := herodot.NewJSONWriter(logger) return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e, err := reg.ErrorManager().Read(r.Context(), r.URL.Query().Get("error")) + e, err := reg.SelfServiceErrorPersister().Read(r.Context(), x.ParseUUID(r.URL.Query().Get("error"))) require.NoError(t, err) logger.Errorf("Found error in NewErrorTestServer: %s", e) writer.Write(w, r, e) diff --git a/selfservice/errorx/manager.go b/selfservice/errorx/manager.go index 29013dad810..2d5668feece 100644 --- a/selfservice/errorx/manager.go +++ b/selfservice/errorx/manager.go @@ -1,116 +1,57 @@ package errorx import ( - "bytes" "context" - "encoding/json" - stderr "errors" "net/http" "net/url" - "time" - - "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/x/urlx" - "github.com/ory/kratos/schema" "github.com/ory/kratos/x" ) type ( - Manager interface { - Store - - // ForwardError is a simple helper that saves all errors in the store and forwards the HTTP Request - // to the error url, appending the error ID. - ForwardError(ctx context.Context, rw http.ResponseWriter, r *http.Request, errs ...error) - } - - ManagementProvider interface { - // ErrorManager returns the errorx.Manager. - ErrorManager() Manager + managerDependencies interface { + PersistenceProvider + x.LoggingProvider + x.WriterProvider } - Store interface { - // Add adds an error to the manager and returns a unique identifier or an error if insertion fails. - Add(ctx context.Context, errs ...error) (string, error) - - // Read returns an error by its unique identifier and marks the error as read. If an error occurs during retrieval - // the second return parameter is an error. - Read(ctx context.Context, id string) ([]json.RawMessage, error) - - // Clear clears read containers that are older than a certain amount of time. If force is set to true, unread - // errors will be cleared as well. - Clear(ctx context.Context, olderThan time.Duration, force bool) error + Manager struct { + d managerDependencies + c baseManagerConfiguration } - baseManagerDependencies interface { - x.LoggingProvider - x.WriterProvider + ManagementProvider interface { + // SelfServiceErrorManager returns the errorx.Manager. + SelfServiceErrorManager() *Manager } baseManagerConfiguration interface { ErrorURL() *url.URL } - - BaseManager struct { - Store - d baseManagerDependencies - c baseManagerConfiguration - } ) -func NewBaseManager(d baseManagerDependencies, c baseManagerConfiguration, m Store) *BaseManager { - return &BaseManager{d: d, c: c, Store: m} +func NewManager(d managerDependencies, c baseManagerConfiguration) *Manager { + return &Manager{d: d, c: c} } -func (m *BaseManager) ForwardError(ctx context.Context, w http.ResponseWriter, r *http.Request, errs ...error) { +// ForwardError is a simple helper that saves all errors in the store and forwards the HTTP Request +// to the error url, appending the error ID. +func (m *Manager) ForwardError(ctx context.Context, w http.ResponseWriter, r *http.Request, errs ...error) { for _, err := range errs { herodot.DefaultErrorLogger(m.d.Logger(), err).Errorf("An error occurred and is being forwarded to the error user interface.") } - id, emerr := m.Add(ctx, errs...) + id, emerr := m.d.SelfServiceErrorPersister().Add(ctx, errs...) if emerr != nil { m.d.Writer().WriteError(w, r, emerr) return } q := url.Values{} - q.Set("error", id) + q.Set("error", id.String()) to := urlx.CopyWithQuery(m.c.ErrorURL(), q).String() http.Redirect(w, r, to, http.StatusFound) } - -func (m *BaseManager) encode(errs []error) (*bytes.Buffer, error) { - es := make([]interface{}, len(errs)) - for k, e := range errs { - e = errors.Cause(e) - if u := stderr.Unwrap(e); u != nil { - e = u - } - - if e == nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithDebug("A nil error was passed to the error manager which is most likely a code bug.")) - } - - // Convert to a default error if the error type is unknown. Helps to properly - // pass through system errors. - switch e.(type) { - case *herodot.DefaultError: - case *schema.ResultErrors: - case schema.ResultErrors: - default: - e = herodot.ToDefaultError(e, "") - } - - es[k] = e - } - - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(es); err != nil { - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to encode error messages.").WithDebug(err.Error())) - } - - return &b, nil -} diff --git a/selfservice/errorx/manager_memory.go b/selfservice/errorx/manager_memory.go deleted file mode 100644 index 53b0966eb5d..00000000000 --- a/selfservice/errorx/manager_memory.go +++ /dev/null @@ -1,92 +0,0 @@ -package errorx - -import ( - "bytes" - "context" - "encoding/json" - "sync" - "time" - - "github.com/pkg/errors" - - "github.com/google/uuid" - - "github.com/ory/herodot" -) - -var _ Manager = new(ManagerMemory) - -type ( - containerMemory struct { - errs []byte - read bool - readAt time.Time - } - - ManagerMemory struct { - sync.RWMutex - containers map[string]containerMemory - *BaseManager - } -) - -func NewManagerMemory( - d baseManagerDependencies, - c baseManagerConfiguration, -) *ManagerMemory { - m := &ManagerMemory{containers: make(map[string]containerMemory)} - m.BaseManager = NewBaseManager(d, c, m) - return m -} - -func (m *ManagerMemory) Add(ctx context.Context, errs ...error) (string, error) { - b, err := m.encode(errs) - if err != nil { - return "", err - } - - id := uuid.New().String() - - m.Lock() - m.containers[id] = containerMemory{ - errs: b.Bytes(), - } - m.Unlock() - - return id, nil -} - -func (m *ManagerMemory) Read(ctx context.Context, id string) ([]json.RawMessage, error) { - m.RLock() - c, ok := m.containers[id] - m.RUnlock() - if !ok { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find error with id: %s", id)) - } - - c.read = true - c.readAt = time.Now() - - m.Lock() - m.containers[id] = c - m.Unlock() - - var errs []json.RawMessage - if err := json.NewDecoder(bytes.NewReader(c.errs)).Decode(&errs); err != nil { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to decode errors.").WithDebug(err.Error())) - } - - return errs, nil -} - -func (m *ManagerMemory) Clear(ctx context.Context, olderThan time.Duration, force bool) error { - m.Lock() - defer m.Unlock() - for k, c := range m.containers { - if (c.read || force) && c.readAt.Before(time.Now().Add(-olderThan)) { - delete(m.containers, k) - } - } - - return nil -} diff --git a/selfservice/errorx/manager_sql.go b/selfservice/errorx/manager_sql.go deleted file mode 100644 index 61ce5c1e973..00000000000 --- a/selfservice/errorx/manager_sql.go +++ /dev/null @@ -1,113 +0,0 @@ -package errorx - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/ory/x/sqlcon" - "github.com/ory/x/sqlxx" - - "github.com/google/uuid" - - "github.com/ory/herodot" -) - -var _ Manager = new(ManagerSQL) - -type ( - containerSQL struct { - PK uint64 `db:"pk"` - ID string `db:"id"` - Errors string `db:"errors"` - SeenAt time.Time `db:"seen_at"` - WasSeen bool `db:"was_seen"` - } - - ManagerSQL struct { - db *sqlx.DB - *BaseManager - } -) - -func NewManagerSQL( - db *sqlx.DB, - d baseManagerDependencies, - c baseManagerConfiguration, -) *ManagerSQL { - m := &ManagerSQL{db: db} - m.BaseManager = NewBaseManager(d, c, m) - return m -} - -func (m *ManagerSQL) Add(ctx context.Context, errs ...error) (string, error) { - b, err := m.encode(errs) - if err != nil { - return "", err - } - - container := &containerSQL{ID: uuid.New().String(), Errors: b.String()} - columns, arguments := sqlxx.NamedInsertArguments(container, "pk", "seen_at") - query := fmt.Sprintf(`INSERT INTO self_service_error (%s) VALUES (%s)`, columns, arguments) - if _, err := m.db.NamedExecContext( - ctx, - query, - container, - ); err != nil { - return "", errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to store error messages in SQL datastore.").WithDebug(err.Error())) - } - - return container.ID, nil -} - -func (m *ManagerSQL) Read(ctx context.Context, id string) ([]json.RawMessage, error) { - var c string - - tx, err := m.db.BeginTxx(ctx, nil) - if err != nil { - return nil, sqlcon.HandleError(err) - } - - if err := tx.GetContext(context.Background(), &c, m.db.Rebind("SELECT errors FROM self_service_error WHERE id=?"), id); err != nil { - if err := tx.Rollback(); err != nil { - return nil, sqlcon.HandleError(err) - } - return nil, sqlcon.HandleError(err) - } - - var errs []json.RawMessage - if err := json.NewDecoder(bytes.NewBufferString(c)).Decode(&errs); err != nil { - if err := tx.Rollback(); err != nil { - return nil, sqlcon.HandleError(err) - } - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to decode stored error messages from SQL datastore.").WithDebug(err.Error())) - } - - if _, err := tx.ExecContext(context.Background(), m.db.Rebind("UPDATE self_service_error SET was_seen = true, seen_at = ? WHERE id = ?"), time.Now().UTC(), id); err != nil { - if err := tx.Rollback(); err != nil { - return nil, sqlcon.HandleError(err) - } - return nil, errors.WithStack(herodot.ErrInternalServerError.WithReason("Unable to update seen status for error message in SQL datastore.").WithDebug(err.Error())) - } - - if err := tx.Commit(); err != nil { - return nil, sqlcon.HandleError(err) - } - - return errs, nil -} - -func (m *ManagerSQL) Clear(ctx context.Context, olderThan time.Duration, force bool) (err error) { - if force { - _, err = m.db.ExecContext(ctx, m.db.Rebind("DELETE FROM self_service_error WHERE seen_at < ?"), olderThan) - } else { - _, err = m.db.ExecContext(ctx, m.db.Rebind("DELETE FROM self_service_error WHERE was_seen=true AND seen_at < ?"), time.Now().UTC().Add(-olderThan)) - } - - return sqlcon.HandleError(err) -} diff --git a/selfservice/errorx/manager_test.go b/selfservice/errorx/manager_test.go deleted file mode 100644 index c30cec19072..00000000000 --- a/selfservice/errorx/manager_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package errorx_test - -import ( - "bytes" - "context" - "encoding/json" - "flag" - "fmt" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/x/sqlcon/dockertest" - - "github.com/ory/herodot" - - "github.com/ory/kratos/internal" - . "github.com/ory/kratos/selfservice/errorx" -) - -// nolint: staticcheck -func TestMain(m *testing.M) { - flag.Parse() - runner := dockertest.Register() - runner.Exit(m.Run()) -} - -func TestManager(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) - var managers = map[string]Manager{ - "memory": NewManagerMemory(reg, conf), - } - - if !testing.Short() { - var l sync.Mutex - dockertest.Parallel([]func(){ - func() { - db, err := dockertest.ConnectToTestPostgreSQL() - require.NoError(t, err) - - _, reg := internal.NewRegistrySQL(t, db) - - l.Lock() - managers["postgres"] = reg.ErrorManager() - l.Unlock() - }, - }) - } - - for n, m := range managers { - t.Run(fmt.Sprintf("manager=%s", n), func(t *testing.T) { - sent := herodot.ErrNotFound.WithReason("foobar") - id, err := m.Add(context.Background(), sent) - require.NoError(t, err) - - var expected bytes.Buffer - require.NoError(t, json.NewEncoder(&expected).Encode([]error{sent})) - - actual, err := m.Read(context.Background(), id) - require.NoError(t, err) - - actualS, _ := json.Marshal(actual) - assert.JSONEq(t, expected.String(), string(actualS)) - - require.NoError(t, m.Clear(context.Background(), time.Second, false)) - actual, err = m.Read(context.Background(), id) - require.NoError(t, err) - - gotu, _ := json.Marshal(actual) - sentu, _ := json.Marshal([]error{sent}) - - assert.JSONEq(t, string(sentu), string(gotu)) - - time.Sleep(time.Millisecond * 100) - - require.NoError(t, m.Clear(context.Background(), time.Millisecond, false)) - _, err = m.Read(context.Background(), id) - require.Error(t, err) - }) - } -} diff --git a/selfservice/errorx/persistence.go b/selfservice/errorx/persistence.go new file mode 100644 index 00000000000..fb0faae0944 --- /dev/null +++ b/selfservice/errorx/persistence.go @@ -0,0 +1,76 @@ +package errorx + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/herodot" + + "github.com/ory/kratos/x" +) + +type ( + Persister interface { + // Add adds an error to the manager and returns a unique identifier or an error if insertion fails. + Add(ctx context.Context, errs ...error) (uuid.UUID, error) + + // Read returns an error by its unique identifier and marks the error as read. If an error occurs during retrieval + // the second return parameter is an error. + Read(ctx context.Context, id uuid.UUID) ([]json.RawMessage, error) + + // Clear clears read containers that are older than a certain amount of time. If force is set to true, unread + // errors will be cleared as well. + Clear(ctx context.Context, olderThan time.Duration, force bool) error + } + + PersistenceProvider interface { + SelfServiceErrorPersister() Persister + } +) + +func TestPersister(p Persister) func(t *testing.T) { + toJSON := func(t *testing.T, in interface{}) string { + out, err := json.Marshal(in) + require.NoError(t, err) + return string(out) + } + + return func(t *testing.T) { + t.Run("case=not found", func(t *testing.T) { + _, err := p.Read(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + t.Run("case=en- and decode properly", func(t *testing.T) { + expected := herodot.ErrNotFound.WithReason("foobar") + actualID, err := p.Add(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.Read(context.Background(), actualID) + require.NoError(t, err) + + assert.JSONEq(t, toJSON(t, []error{expected}), toJSON(t, actual)) + }) + + t.Run("case=clear", func(t *testing.T) { + expected := herodot.ErrNotFound.WithReason("foobar") + actualID, err := p.Add(context.Background(), expected) + require.NoError(t, err) + + _, err = p.Read(context.Background(), actualID) + require.NoError(t, err) + + time.Sleep(time.Millisecond * 100) + + require.NoError(t, p.Clear(context.Background(), time.Millisecond, false)) + _, err = p.Read(context.Background(), actualID) + require.Error(t, err) + }) + } +} diff --git a/selfservice/flow/login/error.go b/selfservice/flow/login/error.go index 8deece8004b..8ccd3d0aa09 100644 --- a/selfservice/flow/login/error.go +++ b/selfservice/flow/login/error.go @@ -38,7 +38,6 @@ type ( ErrorHandler struct { d errorHandlerDependencies c configuration.Provider - bd *x.BodyDecoder } ) @@ -63,7 +62,7 @@ func (s *ErrorHandler) HandleLoginError( Warn("Encountered login error.") if rr == nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } else if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, err) @@ -77,17 +76,17 @@ func (s *ErrorHandler) HandleLoginError( } if err := method.Config.ParseError(err); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } if err := s.d.LoginRequestPersister().UpdateLoginRequest(r.Context(), rr.ID, ct, method); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } http.Redirect(w, r, - urlx.CopyWithQuery(s.c.LoginURL(), url.Values{"request": {rr.ID}}).String(), + urlx.CopyWithQuery(s.c.LoginURL(), url.Values{"request": {rr.ID.String()}}).String(), http.StatusFound, ) } diff --git a/selfservice/flow/login/handler.go b/selfservice/flow/login/handler.go index c9c10f45fe0..73aa5c43129 100644 --- a/selfservice/flow/login/handler.go +++ b/selfservice/flow/login/handler.go @@ -5,8 +5,8 @@ import ( "net/url" "github.com/julienschmidt/httprouter" - "github.com/pkg/errors" + "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/driver/configuration" @@ -56,7 +56,7 @@ func (h *Handler) NewLoginRequest(w http.ResponseWriter, r *http.Request, redir } if err := h.d.LoginHookExecutor().PreLoginHook(w, r, a); err != nil { - if errors.Cause(err) == ErrHookAbortRequest { + if errorsx.Cause(err) == ErrHookAbortRequest { return nil } return err @@ -95,9 +95,9 @@ func (h *Handler) NewLoginRequest(w http.ResponseWriter, r *http.Request, redir // 500: genericError func (h *Handler) initLoginRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if err := h.NewLoginRequest(w, r, func(a *Request) string { - return urlx.CopyWithQuery(h.c.LoginURL(), url.Values{"request": {a.ID}}).String() + return urlx.CopyWithQuery(h.c.LoginURL(), url.Values{"request": {a.ID.String()}}).String() }); err != nil { - h.d.ErrorManager().ForwardError(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } } @@ -121,11 +121,11 @@ func (h *Handler) initLoginRequest(w http.ResponseWriter, r *http.Request, ps ht // 302: emptyResponse // 500: genericError func (h *Handler) fetchLoginRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - ar, err := h.d.LoginRequestPersister().GetLoginRequest(r.Context(), r.URL.Query().Get("request")) + ar, err := h.d.LoginRequestPersister().GetLoginRequest(r.Context(), x.ParseUUID(r.URL.Query().Get("request"))) if err != nil { h.d.Writer().WriteError(w, r, err) return } - h.d.Writer().Write(w, r, ar.Declassify()) + h.d.Writer().Write(w, r, ar) } diff --git a/selfservice/flow/login/handler_test.go b/selfservice/flow/login/handler_test.go index 530fdd87393..a62776944db 100644 --- a/selfservice/flow/login/handler_test.go +++ b/selfservice/flow/login/handler_test.go @@ -24,8 +24,12 @@ import ( "github.com/ory/kratos/x" ) +func init() { + internal.RegisterFakes() +} + func TestEnsureSessionRedirect(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) router := x.NewRouterPublic() reg.LoginHandler().RegisterPublicRoutes(router) @@ -60,7 +64,7 @@ func TestEnsureSessionRedirect(t *testing.T) { } func TestLoginHandler(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) kratos := func() *httptest.Server { router := x.NewRouterPublic() diff --git a/selfservice/flow/login/hook.go b/selfservice/flow/login/hook.go index 40588b2e94d..ffbbf0e7d70 100644 --- a/selfservice/flow/login/hook.go +++ b/selfservice/flow/login/hook.go @@ -49,7 +49,7 @@ func (e *HookExecutor) PostLoginHook(w http.ResponseWriter, r *http.Request, hoo } if s.WasIdentityModified() { - if _, err := e.d.IdentityPool().Update(r.Context(), s.Identity); err != nil { + if err := e.d.IdentityPool().UpdateIdentity(r.Context(), s.Identity); err != nil { return err } } diff --git a/selfservice/flow/login/hook_test.go b/selfservice/flow/login/hook_test.go index 27d4d3f9a2f..61d8447be9a 100644 --- a/selfservice/flow/login/hook_test.go +++ b/selfservice/flow/login/hook_test.go @@ -2,7 +2,6 @@ package login_test import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -86,18 +85,17 @@ func TestLoginExecutor(t *testing.T) { }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) + conf, reg := internal.NewRegistryDefault(t) var i identity.Identity require.NoError(t, faker.FakeData(&i)) i.TraitsSchemaURL = "" - i.Traits = json.RawMessage(`{}`) + i.Traits = identity.Traits(`{}`) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/login.schema.json") - _, err := reg.IdentityPool().Create(context.TODO(), &i) - require.NoError(t, err) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.TODO(), &i)) e := login.NewHookExecutor(reg, conf) - err = e.PostLoginHook(nil, &http.Request{}, tc.hooks, nil, &i) + err := e.PostLoginHook(nil, &http.Request{}, tc.hooks, nil, &i) if tc.expectErr != nil { require.EqualError(t, err, tc.expectErr.Error()) return @@ -105,7 +103,7 @@ func TestLoginExecutor(t *testing.T) { require.NoError(t, err) if tc.expectSchemaURL != "" { - got, err := reg.IdentityPool().Get(context.TODO(), i.ID) + got, err := reg.IdentityPool().GetIdentity(context.TODO(), i.ID) require.NoError(t, err) assert.EqualValues(t, tc.expectSchemaURL, got.TraitsSchemaURL) } @@ -125,7 +123,7 @@ func TestLoginExecutor(t *testing.T) { {reg: &loginExecutorDependenciesMock{preErr: []error{nil}}}, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, _ := internal.NewMemoryRegistry(t) + conf, _ := internal.NewRegistryDefault(t) e := login.NewHookExecutor(tc.reg, conf) if tc.expectErr == nil { require.NoError(t, e.PreLoginHook(nil, nil, nil)) diff --git a/selfservice/flow/login/persistence.go b/selfservice/flow/login/persistence.go index 205bc85e4b1..90cedfd3e76 100644 --- a/selfservice/flow/login/persistence.go +++ b/selfservice/flow/login/persistence.go @@ -5,73 +5,114 @@ import ( "testing" "github.com/bxcodec/faker" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/form" + "github.com/ory/kratos/x" ) type ( RequestPersister interface { CreateLoginRequest(context.Context, *Request) error - GetLoginRequest(ctx context.Context, id string) (*Request, error) - UpdateLoginRequest(context.Context, string, identity.CredentialsType, *RequestMethod) error + GetLoginRequest(context.Context, uuid.UUID) (*Request, error) + UpdateLoginRequest(context.Context, uuid.UUID, identity.CredentialsType, *RequestMethod) error } RequestPersistenceProvider interface { LoginRequestPersister() RequestPersister } ) -func TestRequestPersister(t *testing.T, p RequestPersister) { - // nbr := func() *Request { - // return &Request{ - // ID: uuid.New().String(), - // IssuedAt: time.Now().UTC().Round(time.Second), - // ExpiresAt: time.Now().Add(time.Hour).UTC().Round(time.Second), - // RequestURL: "https://www.ory.sh/request", - // RequestHeaders: http.Header{"Content-Type": {"application/json"}}, - // // Disable Active as this value is initially empty (NULL). - // // Active: identity.CredentialsTypePassword, - // Methods: map[identity.CredentialsType]*CredentialsRequest{ - // identity.CredentialsTypePassword: { - // Method: identity.CredentialsTypePassword, - // Config: password.NewRequestMethodConfig(), - // }, - // identity.CredentialsTypeOIDC: { - // Method: identity.CredentialsTypeOIDC, - // Config: oidc.NewRequestMethodConfig(), - // }, - // }, - // } - // } - // - // assertUpdated := func(t *testing.T, expected, actual Request) { - // assert.EqualValues(t, identity.CredentialsTypePassword, actual.Active) - // assert.EqualValues(t, "bar", actual.Methods[identity.CredentialsTypeOIDC].Config.(*oidc.RequestMethodConfig).Action) - // assert.EqualValues(t, "foo", actual.Methods[identity.CredentialsTypePassword].Config.(*password.RequestMethodConfig).Action) - // } - - t.Run("case=should error when the login request does not exist", func(t *testing.T) { - _, err := p.GetLoginRequest(context.Background(), "does-not-exist") - require.NoError(t, err) - }) - - t.Run("case=", func(t *testing.T) { - var r Request - require.NoError(t, faker.FakeData(&r)) - t.Logf("%+v", r) - }) - - // r := LoginRequest{Request: nbr()} - // require.NoError(t, p.CreateLoginRequest(context.Background(), &r)) - // - // g, err := p.GetLoginRequest(context.Background(), r.ID) - // require.NoError(t, err) - // assert.EqualValues(t, r, *g) - // - // require.NoError(t, p.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypeOIDC, &oidc.RequestMethod{Action: "bar"})) - // require.NoError(t, p.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypePassword, &password.RequestMethod{Action: "foo"})) - // - // g, err = p.GetLoginRequest(context.Background(), r.ID) - // require.NoError(t, err) - // assertUpdated(t, *r.Request, *g.Request) +func TestRequestPersister(p RequestPersister) func(t *testing.T) { + var clearids = func(r *Request) { + r.ID = uuid.UUID{} + for k := range r.Methods { + r.Methods[k].ID = uuid.UUID{} + } + } + + return func(t *testing.T) { + t.Run("case=should error when the login request does not exist", func(t *testing.T) { + _, err := p.GetLoginRequest(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + var newRequest = func(t *testing.T) *Request { + var r Request + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + + methods := len(r.Methods) + assert.NotZero(t, methods) + + return &r + } + + t.Run("case=should create with set ids", func(t *testing.T) { + var r Request + require.NoError(t, faker.FakeData(&r)) + require.NoError(t, p.CreateLoginRequest(context.Background(), &r)) + }) + + t.Run("case=should create a new login request and properly set IDs", func(t *testing.T) { + r := newRequest(t) + methods := len(r.Methods) + err := p.CreateLoginRequest(context.Background(), r) + require.NoError(t, err, "%#v", err) + + assert.Nil(t, r.MethodsRaw) + assert.NotEqual(t, uuid.Nil, r.ID) + for _, m := range r.Methods { + assert.NotEqual(t, uuid.Nil, m.ID) + } + assert.Len(t, r.Methods, methods) + }) + + t.Run("case=should create and fetch a login request", func(t *testing.T) { + expected := newRequest(t) + err := p.CreateLoginRequest(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.GetLoginRequest(context.Background(), expected.ID) + require.NoError(t, err) + assert.Empty(t, actual.MethodsRaw) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Active, actual.Active) + require.Equal(t, len(expected.Methods), len(actual.Methods), "expected:\t%s\nactual:\t%s", expected.Methods, actual.Methods) + }) + + t.Run("case=should update a login request", func(t *testing.T) { + expected := newRequest(t) + delete(expected.Methods, identity.CredentialsTypeOIDC) + err := p.CreateLoginRequest(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.GetLoginRequest(context.Background(), expected.ID) + require.NoError(t, err) + assert.Len(t, actual.Methods, 1) + + require.NoError(t, p.UpdateLoginRequest(context.Background(), expected.ID, identity.CredentialsTypeOIDC, &RequestMethod{ + Method: identity.CredentialsTypeOIDC, + Config: &RequestMethodConfig{form.NewHTMLForm(string(identity.CredentialsTypeOIDC))}, + })) + + require.NoError(t, p.UpdateLoginRequest(context.Background(), expected.ID, identity.CredentialsTypePassword, &RequestMethod{ + Method: identity.CredentialsTypePassword, + Config: &RequestMethodConfig{form.NewHTMLForm(string(identity.CredentialsTypePassword))}, + })) + + actual, err = p.GetLoginRequest(context.Background(), expected.ID) + require.NoError(t, err) + require.Len(t, actual.Methods, 2) + + assert.Equal(t, string(identity.CredentialsTypePassword), actual.Methods[identity.CredentialsTypePassword].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) + assert.Equal(t, string(identity.CredentialsTypeOIDC), actual.Methods[identity.CredentialsTypeOIDC].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) + }) + } } diff --git a/selfservice/flow/login/request.go b/selfservice/flow/login/request.go index 692e57b20e2..c46ecf23662 100644 --- a/selfservice/flow/login/request.go +++ b/selfservice/flow/login/request.go @@ -4,59 +4,50 @@ import ( "net/http" "time" - "github.com/google/uuid" + "github.com/gobuffalo/pop" + "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/x/urlx" "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/form" + "github.com/ory/kratos/x" ) -// swagger:model loginRequestMethod -type RequestMethod struct { - // Method contains the request credentials type. - Method identity.CredentialsType `json:"method"` - - // Config is the credential type's config. - Config RequestMethodConfig `json:"config"` -} - -// swagger:model loginRequestMethodConfig -type RequestMethodConfig interface { - form.ErrorParser - form.ValueSetter - form.Resetter - form.CSRFSetter -} - // swagger:model loginRequest type Request struct { // ID represents the request's unique ID. When performing the login flow, this // represents the id in the login ui's query parameter: http:///?request= - ID string `json:"id"` + ID uuid.UUID `json:"id" faker:"uuid" rw:"r" db:"id"` // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to log in, // a new request has to be initiated. - ExpiresAt time.Time `json:"expires_at" faker:"time_type"` + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` // IssuedAt is the time (UTC) when the request occurred. - IssuedAt time.Time `json:"issued_at" faker:"time_type"` + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` // RequestURL is the initial URL that was requested from ORY Kratos. It can be used // to forward information contained in the URL's path or query for example. - RequestURL string `json:"request_url"` + RequestURL string `json:"request_url" db:"request_url"` // Active, if set, contains the login method that is being used. It is initially // not set. - Active identity.CredentialsType `json:"active,omitempty"` + Active identity.CredentialsType `json:"active,omitempty" db:"active_method"` // Methods contains context for all enabled login methods. If a login request has been // processed, but for example the password is incorrect, this will contain error messages. - Methods map[identity.CredentialsType]*RequestMethod `json:"methods" faker:"login_request_methods"` + Methods map[identity.CredentialsType]*RequestMethod `json:"methods" faker:"login_request_methods" db:"-"` + + // MethodsRaw is a helper struct field for gobuffalo.pop. + MethodsRaw RequestMethodsRaw `json:"-" faker:"-" has_many:"selfservice_login_request_methods" fk_id:"selfservice_login_request_id"` - RequestHeaders http.Header `json:"-" faker:"http_header"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` } func NewLoginRequest(exp time.Duration, r *http.Request) *Request { @@ -71,13 +62,44 @@ func NewLoginRequest(exp time.Duration, r *http.Request) *Request { } return &Request{ - ID: uuid.New().String(), - ExpiresAt: time.Now().UTC().Add(exp), - IssuedAt: time.Now().UTC(), - RequestURL: source.String(), - RequestHeaders: r.Header, - Methods: map[identity.CredentialsType]*RequestMethod{}, + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(exp), + IssuedAt: time.Now().UTC(), + RequestURL: source.String(), + Methods: map[identity.CredentialsType]*RequestMethod{}, + } +} + +func (r *Request) BeforeSave(_ *pop.Connection) error { + r.MethodsRaw = make([]RequestMethod, 0, len(r.Methods)) + for _, m := range r.Methods { + r.MethodsRaw = append(r.MethodsRaw, *m) } + r.Methods = nil + return nil +} + +func (r *Request) AfterCreate(c *pop.Connection) error { + return r.AfterFind(c) +} + +func (r *Request) AfterUpdate(c *pop.Connection) error { + return r.AfterFind(c) +} + +func (r *Request) AfterFind(_ *pop.Connection) error { + r.Methods = make(RequestMethods) + for key := range r.MethodsRaw { + m := r.MethodsRaw[key] // required for pointer dereference + r.Methods[m.Method] = &m + } + r.MethodsRaw = nil + return nil +} + +func (r Request) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_login_requests" } func (r *Request) Valid() error { @@ -91,14 +113,6 @@ func (r *Request) Valid() error { return nil } -func (r *Request) GetID() string { +func (r *Request) GetID() uuid.UUID { return r.ID } - -// Declassify returns a copy of the Request where all sensitive information -// such as request headers is removed. -func (r *Request) Declassify() *Request { - rr := *r - rr.RequestHeaders = http.Header{} - return &rr -} diff --git a/selfservice/flow/login/request_method.go b/selfservice/flow/login/request_method.go new file mode 100644 index 00000000000..56bb4e6ee13 --- /dev/null +++ b/selfservice/flow/login/request_method.go @@ -0,0 +1,84 @@ +package login + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/aliases" + "github.com/ory/kratos/selfservice/form" +) + +// swagger:model loginRequestMethod +type RequestMethod struct { + // Method contains the request credentials type. + Method identity.CredentialsType `json:"method" db:"method"` + + // Config is the credential type's config. + Config *RequestMethodConfig `json:"config" db:"config"` + + // ID is a helper struct field for gobuffalo.pop. + ID uuid.UUID `json:"-" db:"id" rw:"r"` + + // RequestID is a helper struct field for gobuffalo.pop. + RequestID uuid.UUID `json:"-" db:"selfservice_login_request_id"` + + // Request is a helper struct field for gobuffalo.pop. + Request *Request `json:"-" belongs_to:"selfservice_login_request" fk_id:"RequestID"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` +} + +func (u RequestMethod) TableName() string { + return "selfservice_login_request_methods" +} + +type RequestMethodsRaw []RequestMethod // workaround for https://github.com/gobuffalo/pop/pull/478 +type RequestMethods map[identity.CredentialsType]*RequestMethod + +func (u RequestMethodsRaw) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_login_request_methods" +} + +func (u RequestMethods) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_login_request_methods" +} + +// swagger:ignore +type RequestMethodConfigurator interface { + form.ErrorParser + form.ValueSetter + form.Resetter + form.CSRFSetter +} + +// swagger:model loginRequestMethodConfig +type RequestMethodConfig struct { + RequestMethodConfigurator +} + +func (c *RequestMethodConfig) Scan(value interface{}) error { + return aliases.JSONScan(c, value) +} + +func (c *RequestMethodConfig) Value() (driver.Value, error) { + return aliases.JSONValue(c) +} + +func (c *RequestMethodConfig) UnmarshalJSON(data []byte) error { + c.RequestMethodConfigurator = new(form.HTMLForm) + return json.Unmarshal(data, c.RequestMethodConfigurator) +} + +func (c *RequestMethodConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(c.RequestMethodConfigurator) +} diff --git a/selfservice/flow/login/request_test.go b/selfservice/flow/login/request_test.go index e9313c52ff2..ba7bd2495d6 100644 --- a/selfservice/flow/login/request_test.go +++ b/selfservice/flow/login/request_test.go @@ -8,13 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow/login" + "github.com/ory/kratos/x" ) func TestFakeRequestData(t *testing.T) { - internal.RegisterFakes() - var r login.Request require.NoError(t, faker.FakeData(&r)) @@ -28,18 +26,11 @@ func TestFakeRequestData(t *testing.T) { assert.NotEmpty(t, m.Method) assert.NotEmpty(t, m.Config) } - - assert.NotEmpty(t, r.RequestHeaders) - for k, v := range r.RequestHeaders { - assert.NotEmpty(t, k) - assert.NotEmpty(t, v) - } } func TestRequest(t *testing.T) { - r := &login.Request{ID: "request", RequestHeaders: map[string][]string{"foo": {"bar"}}} + r := &login.Request{ID: x.NewUUID()} assert.Equal(t, r.ID, r.GetID()) - assert.Empty(t, r.Declassify().RequestHeaders) t.Run("case=expired", func(t *testing.T) { for _, tc := range []struct { diff --git a/selfservice/flow/logout/handler.go b/selfservice/flow/logout/handler.go index 3d7f590211b..43807cb9419 100644 --- a/selfservice/flow/logout/handler.go +++ b/selfservice/flow/logout/handler.go @@ -42,7 +42,7 @@ func (h *Handler) logout(w http.ResponseWriter, r *http.Request, ps httprouter.P _ = h.d.CSRFHandler().RegenerateToken(w, r) if err := h.d.SessionManager().PurgeFromRequest(r.Context(), w, r); err != nil { - h.d.ErrorManager().ForwardError(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } diff --git a/selfservice/flow/logout/handler_test.go b/selfservice/flow/logout/handler_test.go index 26168464de2..c39d7ec42a0 100644 --- a/selfservice/flow/logout/handler_test.go +++ b/selfservice/flow/logout/handler_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/gobuffalo/httptest" - "github.com/google/uuid" "github.com/julienschmidt/httprouter" "github.com/justinas/nosurf" "github.com/sirupsen/logrus" @@ -25,9 +24,11 @@ import ( ) func TestLogoutHandler(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) handler := reg.LogoutHandler() + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") + router := x.NewRouterPublic() handler.RegisterPublicRoutes(router) reg.WithCSRFHandler(x.NewCSRFHandler(router, reg.Writer(), logrus.New(), "/", "", false)) @@ -35,9 +36,10 @@ func TestLogoutHandler(t *testing.T) { defer ts.Close() var sess session.Session - sess.SID = uuid.New().String() + sess.ID = x.NewUUID() sess.Identity = new(identity.Identity) - require.NoError(t, reg.SessionManager().Create(context.Background(), &sess)) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), sess.Identity)) + require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), &sess)) router.GET("/set", session.MockSetSession(t, reg)) @@ -50,7 +52,6 @@ func TestLogoutHandler(t *testing.T) { })) defer redirTS.Close() - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") viper.Set(configuration.ViperKeySelfServiceLogoutRedirectURL, redirTS.URL) viper.Set(configuration.ViperKeyURLsSelfPublic, ts.URL) diff --git a/selfservice/flow/profile/error.go b/selfservice/flow/profile/error.go index 27f0cab6a20..68b4e0d4385 100644 --- a/selfservice/flow/profile/error.go +++ b/selfservice/flow/profile/error.go @@ -34,7 +34,6 @@ type ( ErrorHandler struct { d errorHandlerDependencies c configuration.Provider - bd *x.BodyDecoder } ) @@ -58,7 +57,7 @@ func (s *ErrorHandler) HandleProfileManagementError( Warn("Encountered profile management error.") if rr == nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } else if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, err) @@ -66,17 +65,17 @@ func (s *ErrorHandler) HandleProfileManagementError( } if err := rr.Form.ParseError(err); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } - if err := s.d.ProfileRequestPersister().UpdateProfileRequest(r.Context(), rr.ID, rr); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + if err := s.d.ProfileRequestPersister().UpdateProfileRequest(r.Context(), rr); err != nil { + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } http.Redirect(w, r, - urlx.CopyWithQuery(s.c.ProfileURL(), url.Values{"request": {rr.ID}}).String(), + urlx.CopyWithQuery(s.c.ProfileURL(), url.Values{"request": {rr.ID.String()}}).String(), http.StatusFound, ) } diff --git a/selfservice/flow/profile/handler.go b/selfservice/flow/profile/handler.go index baea0047388..a50c9a0793e 100644 --- a/selfservice/flow/profile/handler.go +++ b/selfservice/flow/profile/handler.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" "net/url" - "reflect" + "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/justinas/nosurf" "github.com/pkg/errors" @@ -33,6 +33,7 @@ type ( handlerDependencies interface { x.CSRFProvider x.WriterProvider + x.LoggingProvider session.HandlerProvider session.ManagementProvider @@ -86,25 +87,26 @@ func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) { func (h *Handler) initUpdateProfile(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { s, err := h.d.SessionManager().FetchFromRequest(r.Context(), w, r) if err != nil { - h.d.ErrorManager().ForwardError(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } a := NewRequest(h.c.SelfServiceProfileRequestLifespan(), r, s) - a.Form = form.NewHTMLFormFromJSON(urlx.AppendPaths(h.c.SelfPublicURL(), BrowserProfilePath).String(), s.Identity.Traits, "traits") + a.Form = form.NewHTMLFormFromJSON(urlx.AppendPaths(h.c.SelfPublicURL(), BrowserProfilePath).String(), json.RawMessage(s.Identity.Traits), "traits") if err := h.d.ProfileRequestPersister().CreateProfileRequest(r.Context(), a); err != nil { - h.d.ErrorManager().ForwardError(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } http.Redirect(w, r, - urlx.CopyWithQuery(h.c.ProfileURL(), url.Values{"request": {a.ID}}).String(), + urlx.CopyWithQuery(h.c.ProfileURL(), url.Values{"request": {a.ID.String()}}).String(), http.StatusFound, ) } // swagger:parameters getProfileManagementRequest type ( + // nolint:deadcode,unused getProfileManagementRequestParameters struct { // Request should be set to the value of the `request` query parameter // by the profile management UI. @@ -139,6 +141,8 @@ type ( // 200: profileManagementRequest // 302: emptyResponse // 500: genericError +// +// nolint:deadcode,unused func fetchUpdateProfileRequestAdmin() {} // swagger:route GET /profiles/requests public getProfileManagementRequest @@ -166,7 +170,7 @@ func fetchUpdateProfileRequestAdmin() {} // 302: emptyResponse // 500: genericError func (h *Handler) fetchUpdateProfileRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - rid := r.URL.Query().Get("request") + rid := x.ParseUUID(r.URL.Query().Get("request")) ar, err := h.d.ProfileRequestPersister().GetProfileRequest(r.Context(), rid) if err != nil { h.d.Writer().WriteError(w, r, err) @@ -179,17 +183,11 @@ func (h *Handler) fetchUpdateProfileRequest(w http.ResponseWriter, r *http.Reque return } - if ar.identityID != sess.Identity.ID { + if ar.IdentityID != sess.Identity.ID { h.d.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden.WithReasonf("The request was made for another identity and has been blocked for security reasons."))) return } - i, err := h.d.IdentityPool().Get(r.Context(), ar.identityID) - if err != nil { - h.d.Writer().WriteError(w, r, err) - return - } - ar.Form.SetField("request", form.Field{ Name: "request", Type: "hidden", @@ -197,13 +195,12 @@ func (h *Handler) fetchUpdateProfileRequest(w http.ResponseWriter, r *http.Reque Value: rid, }) ar.Form.SetCSRF(nosurf.Token(r)) - ar.Identity = i - h.d.Writer().Write(w, r, ar) } type ( // swagger:parameters completeProfileManagementFlow + // nolint:deadcode,unused completeProfileManagementParameters struct { // in: body // required: true @@ -211,6 +208,7 @@ type ( } // swagger:model completeProfileManagementPayload + // nolint:deadcode,unused completeProfileManagementPayload struct { // Traits contains all of the identity's traits. // @@ -223,7 +221,7 @@ type ( // // type: string // required: true - Request string `json:"request"` + Request uuid.UUID `json:"request"` } ) @@ -269,7 +267,7 @@ func (h *Handler) completeProfileManagementFlow(w http.ResponseWriter, r *http.R return } - if len(p.Request) == 0 { + if x.IsZeroUUID(p.Request) { h.handleProfileManagementError(w, r, nil, s.Identity.Traits, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The request query parameter is missing."))) return } @@ -292,14 +290,14 @@ func (h *Handler) completeProfileManagementFlow(w http.ResponseWriter, r *http.R // identity.TraitsSchemaURL - creds, err := h.d.IdentityPool().GetClassified(r.Context(), s.Identity.ID) + creds, err := h.d.IdentityPool().GetIdentityConfidential(r.Context(), s.Identity.ID) if err != nil { - h.handleProfileManagementError(w, r, ar, p.Traits, err) + h.handleProfileManagementError(w, r, ar, identity.Traits(p.Traits), err) return } i := *s.Identity - i.Traits = p.Traits + i.Traits = identity.Traits(p.Traits) i.Credentials = creds.CopyCredentials() // If credential identifiers have changed we need to block this action UNLESS @@ -314,7 +312,17 @@ func (h *Handler) completeProfileManagementFlow(w http.ResponseWriter, r *http.R } // Check if any credentials-related field changed. - if !reflect.DeepEqual(creds.Credentials, i.Credentials) { + if !i.CredentialsEqual(creds.Credentials) { + + // !! WARNING !! + // + // This will leak the credential options which may include the hashed password. Do not use seriously: + // + // h.d.Logger(). + // WithField("original_credentials", fmt.Sprintf("%+v", creds.Credentials)). + // WithField("updated_credentials", fmt.Sprintf("%+v", i.Credentials)). + // Trace("Credentials changed unexpectedly in CompleteProfileManagementFlow.") + h.handleProfileManagementError(w, r, ar, i.Traits, errors.WithStack( herodot.ErrInternalServerError. @@ -323,39 +331,39 @@ func (h *Handler) completeProfileManagementFlow(w http.ResponseWriter, r *http.R return } - if _, err := h.d.IdentityPool().Update(r.Context(), &i); err != nil { + if err := h.d.IdentityPool().UpdateIdentity(r.Context(), &i); err != nil { h.handleProfileManagementError(w, r, ar, i.Traits, err) return } ar.Form.Reset() ar.UpdateSuccessful = true - for name, field := range form.NewHTMLFormFromJSON("", i.Traits, "traits").Fields { + for name, field := range form.NewHTMLFormFromJSON("", json.RawMessage(i.Traits), "traits").Fields { ar.Form.SetField(name, field) } ar.Form.SetValue("request", r.Form.Get("request")) ar.Form.SetCSRF(nosurf.Token(r)) - if err := h.d.ProfileRequestPersister().UpdateProfileRequest(r.Context(), ar.ID, ar); err != nil { + if err := h.d.ProfileRequestPersister().UpdateProfileRequest(r.Context(), ar); err != nil { h.handleProfileManagementError(w, r, ar, i.Traits, err) return } http.Redirect(w, r, - urlx.CopyWithQuery(h.c.ProfileURL(), url.Values{"request": {ar.ID}}).String(), + urlx.CopyWithQuery(h.c.ProfileURL(), url.Values{"request": {ar.ID.String()}}).String(), http.StatusFound, ) } // handleProfileManagementError is a convenience function for handling all types of errors that may occur (e.g. validation error) // during a profile management request. -func (h *Handler) handleProfileManagementError(w http.ResponseWriter, r *http.Request, rr *Request, traits json.RawMessage, err error) { +func (h *Handler) handleProfileManagementError(w http.ResponseWriter, r *http.Request, rr *Request, traits identity.Traits, err error) { if rr != nil { rr.Form.Reset() rr.UpdateSuccessful = false if traits != nil { - for name, field := range form.NewHTMLFormFromJSON("", traits, "traits").Fields { + for name, field := range form.NewHTMLFormFromJSON("", json.RawMessage(traits), "traits").Fields { rr.Form.SetField(name, field) } } diff --git a/selfservice/flow/profile/handler_test.go b/selfservice/flow/profile/handler_test.go index 5a4dd66eced..75434a6689b 100644 --- a/selfservice/flow/profile/handler_test.go +++ b/selfservice/flow/profile/handler_test.go @@ -11,7 +11,6 @@ import ( "testing" "github.com/go-openapi/runtime" - "github.com/google/uuid" "github.com/julienschmidt/httprouter" "github.com/justinas/nosurf" "github.com/stretchr/testify/assert" @@ -50,7 +49,7 @@ func fieldsToURLValues(ff models.FormFields) url.Values { } func TestUpdateProfile(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") ui := func() *httptest.Server { @@ -76,12 +75,12 @@ func TestUpdateProfile(t *testing.T) { viper.Set(configuration.ViperKeyURLsLogin, ui.URL+"/login") primaryIdentity := &identity.Identity{ - ID: uuid.New().String(), + ID: x.NewUUID(), Credentials: map[identity.CredentialsType]identity.Credentials{ - "password": {ID: "password", Identifiers: []string{"john@doe.com"}, Config: json.RawMessage(`{"hashed_password":"foo"}`)}, + "password": {Type: "password", Identifiers: []string{"john@doe.com"}, Config: json.RawMessage(`{"hashed_password":"foo"}`)}, }, TraitsSchemaURL: "file://./stub/identity.schema.json", - Traits: json.RawMessage(`{"email":"john@doe.com","stringy":"foobar","booly":false,"numby":2.5}`), + Traits: identity.Traits(`{"email":"john@doe.com","stringy":"foobar","booly":false,"numby":2.5}`), } kratos := func() *httptest.Server { @@ -90,7 +89,7 @@ func TestUpdateProfile(t *testing.T) { route, _ := session.MockSessionCreateHandlerWithIdentity(t, reg, primaryIdentity) router.GET("/setSession", route) - other, _ := session.MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{ID: uuid.New().String(), TraitsSchemaURL: "file://./stub/identity.schema.json", Traits: json.RawMessage(`{}`)}) + other, _ := session.MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{ID: x.NewUUID(), TraitsSchemaURL: "file://./stub/identity.schema.json", Traits: identity.Traits(`{}`)}) router.GET("/setSession/other-user", other) n := negroni.Classic() n.UseHandler(router) @@ -197,10 +196,9 @@ func TestUpdateProfile(t *testing.T) { ) require.NoError(t, err, "%s", rid) - assert.Equal(t, rid, pr.Payload.ID) + assert.Equal(t, rid, string(pr.Payload.ID)) assert.NotEmpty(t, pr.Payload.Identity) - assert.Empty(t, pr.Payload.Identity.Credentials) - assert.Equal(t, primaryIdentity.ID, *(pr.Payload.Identity.ID)) + assert.Equal(t, primaryIdentity.ID.String(), string(pr.Payload.Identity.ID)) assert.JSONEq(t, string(primaryIdentity.Traits), x.MustEncodeJSON(t, pr.Payload.Identity.Traits)) assert.Equal(t, primaryIdentity.TraitsSchemaURL, pr.Payload.Identity.TraitsSchemaURL) assert.Equal(t, kratos.URL+profile.BrowserProfilePath, pr.Payload.RequestURL) diff --git a/selfservice/flow/profile/persistence.go b/selfservice/flow/profile/persistence.go index fcb2397eaf5..b92d1407db7 100644 --- a/selfservice/flow/profile/persistence.go +++ b/selfservice/flow/profile/persistence.go @@ -1,14 +1,118 @@ package profile -import "context" +import ( + "context" + "encoding/json" + "testing" + + "github.com/bxcodec/faker" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/viper" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" +) type ( RequestPersister interface { CreateProfileRequest(context.Context, *Request) error - GetProfileRequest(ctx context.Context, id string) (*Request, error) - UpdateProfileRequest(context.Context, string, *Request) error + GetProfileRequest(ctx context.Context, id uuid.UUID) (*Request, error) + UpdateProfileRequest(context.Context, *Request) error } RequestPersistenceProvider interface { ProfileRequestPersister() RequestPersister } ) + +func TestRequestPersister(p interface { + RequestPersister + identity.Pool +}) func(t *testing.T) { + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") + + var clearids = func(r *Request) { + r.ID = uuid.UUID{} + r.Identity.ID = uuid.UUID{} + r.IdentityID = uuid.UUID{} + } + + return func(t *testing.T) { + t.Run("case=should error when the profile request does not exist", func(t *testing.T) { + _, err := p.GetProfileRequest(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + var newRequest = func(t *testing.T) *Request { + var r Request + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + require.NoError(t, p.CreateIdentity(context.Background(), r.Identity)) + return &r + } + + t.Run("case=should create a new profile request", func(t *testing.T) { + r := newRequest(t) + err := p.CreateProfileRequest(context.Background(), r) + require.NoError(t, err, "%#v", err) + + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r Request + require.NoError(t, faker.FakeData(&r)) + require.NoError(t, p.CreateIdentity(context.Background(), r.Identity)) + require.NoError(t, p.CreateProfileRequest(context.Background(), &r)) + }) + + t.Run("case=should create and fetch a profile request", func(t *testing.T) { + expected := newRequest(t) + err := p.CreateProfileRequest(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.GetProfileRequest(context.Background(), expected.ID) + require.NoError(t, err) + + factual, _ := json.Marshal(actual.Form) + fexpected, _ := json.Marshal(expected.Form) + + assert.NotEmpty(t, actual.Form.Action) + assert.EqualValues(t, expected.ID, actual.ID) + assert.JSONEq(t, string(fexpected), string(factual)) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Identity.ID, actual.Identity.ID) + assert.EqualValues(t, expected.Identity.Traits, actual.Identity.Traits) + assert.EqualValues(t, expected.Identity.TraitsSchemaURL, actual.Identity.TraitsSchemaURL) + assert.Empty(t, actual.Identity.Credentials) + }) + + t.Run("case=should fail to create if identity does not exist", func(t *testing.T) { + var expected Request + require.NoError(t, faker.FakeData(&expected)) + clearids(&expected) + err := p.CreateProfileRequest(context.Background(), &expected) + require.Error(t, err) + }) + + t.Run("case=should create and update a profile request", func(t *testing.T) { + expected := newRequest(t) + err := p.CreateProfileRequest(context.Background(), expected) + require.NoError(t, err) + + expected.Form.Action = "/new-action" + expected.RequestURL = "/new-request-url" + require.NoError(t, p.UpdateProfileRequest(context.Background(), expected)) + + actual, err := p.GetProfileRequest(context.Background(), expected.ID) + require.NoError(t, err) + + assert.Equal(t, "/new-action", actual.Form.Action) + assert.Equal(t, "/new-request-url", actual.RequestURL) + }) + } +} diff --git a/selfservice/flow/profile/request.go b/selfservice/flow/profile/request.go index ebac5ea5255..5ce15c0007f 100644 --- a/selfservice/flow/profile/request.go +++ b/selfservice/flow/profile/request.go @@ -4,7 +4,7 @@ import ( "net/http" "time" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/ory/herodot" @@ -13,6 +13,7 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/form" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) // Request presents a profile management request @@ -26,31 +27,39 @@ import ( type Request struct { // ID represents the request's unique ID. When performing the profile management flow, this // represents the id in the profile ui's query parameter: http://?request= - ID string `json:"id"` + // + // type: string + // format: uuid + ID uuid.UUID `json:"id" db:"id" faker:"uuid" rw:"r"` // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to update the profile, // a new request has to be initiated. - ExpiresAt time.Time `json:"expires_at"` + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` // IssuedAt is the time (UTC) when the request occurred. - IssuedAt time.Time `json:"issued_at"` + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` // RequestURL is the initial URL that was requested from ORY Kratos. It can be used // to forward information contained in the URL's path or query for example. - RequestURL string `json:"request_url"` + RequestURL string `json:"request_url" db:"request_url"` // Form contains form fields, errors, and so on. - Form *form.HTMLForm `json:"form"` + Form *form.HTMLForm `json:"form" db:"form"` // Identity contains all of the identity's data in raw form. - Identity *identity.Identity `json:"identity"` + Identity *identity.Identity `json:"identity" faker:"identity" db:"-" belongs_to:"identities" fk_id:"IdentityID"` // UpdateSuccessful, if true, indicates that the profile has been updated successfully with the provided data. // Done will stay true when repeatedly checking. If set to true, done will revert back to false only // when a request with invalid (e.g. "please use a valid phone number") data was sent. - UpdateSuccessful bool `json:"update_successful,omitempty"` - - identityID string `json:"-"` + UpdateSuccessful bool `json:"update_successful,omitempty" faker:"-" db:"update_successful"` + + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` } func NewRequest(exp time.Duration, r *http.Request, s *session.Session) *Request { @@ -65,20 +74,25 @@ func NewRequest(exp time.Duration, r *http.Request, s *session.Session) *Request } return &Request{ - ID: uuid.New().String(), + ID: x.NewUUID(), ExpiresAt: time.Now().UTC().Add(exp), IssuedAt: time.Now().UTC(), RequestURL: source.String(), - identityID: s.Identity.ID, + IdentityID: s.Identity.ID, + Identity: s.Identity, Form: new(form.HTMLForm), } } +func (r *Request) TableName() string { + return "selfservice_profile_management_requests" +} + func (r *Request) Valid(s *session.Session) error { if r.ExpiresAt.Before(time.Now()) { return errors.WithStack(ErrRequestExpired.WithReasonf("The profile request expired %.2f minutes ago, please try again.", time.Since(r.ExpiresAt).Minutes())) } - if r.identityID != s.Identity.ID { + if r.IdentityID != s.Identity.ID { return errors.WithStack(herodot.ErrBadRequest.WithReasonf("The profile request expired %.2f minutes ago, please try again", time.Since(r.ExpiresAt).Minutes())) } return nil diff --git a/selfservice/flow/profile/request_test.go b/selfservice/flow/profile/request_test.go index 6a97a54508f..32f8915f228 100644 --- a/selfservice/flow/profile/request_test.go +++ b/selfservice/flow/profile/request_test.go @@ -13,9 +13,12 @@ import ( "github.com/ory/kratos/identity" "github.com/ory/kratos/selfservice/flow/profile" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) func TestProfileRequest(t *testing.T) { + alice := x.NewUUID() + malice := x.NewUUID() for k, tc := range []struct { r *profile.Request s *session.Session @@ -25,26 +28,26 @@ func TestProfileRequest(t *testing.T) { r: profile.NewRequest( time.Hour, &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, - &session.Session{Identity: &identity.Identity{ID: "alice"}}, + &session.Session{Identity: &identity.Identity{ID: alice}}, ), - s: &session.Session{Identity: &identity.Identity{ID: "alice"}}, + s: &session.Session{Identity: &identity.Identity{ID: alice}}, }, { r: profile.NewRequest( time.Hour, &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, - &session.Session{Identity: &identity.Identity{ID: "alice"}}, + &session.Session{Identity: &identity.Identity{ID: alice}}, ), - s: &session.Session{Identity: &identity.Identity{ID: "malice"}}, + s: &session.Session{Identity: &identity.Identity{ID: malice}}, expectErr: true, }, { r: profile.NewRequest( -time.Hour, &http.Request{URL: urlx.ParseOrPanic("http://foo/bar/baz"), Host: "foo"}, - &session.Session{Identity: &identity.Identity{ID: "alice"}}, + &session.Session{Identity: &identity.Identity{ID: alice}}, ), - s: &session.Session{Identity: &identity.Identity{ID: "alice"}}, + s: &session.Session{Identity: &identity.Identity{ID: alice}}, expectErr: true, }, } { diff --git a/selfservice/flow/registration/error.go b/selfservice/flow/registration/error.go index 4431fcf0215..e2c0c736b0b 100644 --- a/selfservice/flow/registration/error.go +++ b/selfservice/flow/registration/error.go @@ -38,7 +38,6 @@ type ( ErrorHandler struct { d errorHandlerDependencies c configuration.Provider - bd *x.BodyDecoder } ) @@ -63,7 +62,7 @@ func (s *ErrorHandler) HandleRegistrationError( Warn("Encountered login error.") if rr == nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } else if x.IsJSONRequest(r) { s.d.Writer().WriteError(w, r, err) @@ -77,17 +76,17 @@ func (s *ErrorHandler) HandleRegistrationError( } if err := method.Config.ParseError(err); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } if err := s.d.RegistrationRequestPersister().UpdateRegistrationRequest(r.Context(), rr.ID, ct, method); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } http.Redirect(w, r, - urlx.CopyWithQuery(s.c.RegisterURL(), url.Values{"request": {rr.ID}}).String(), + urlx.CopyWithQuery(s.c.RegisterURL(), url.Values{"request": {rr.ID.String()}}).String(), http.StatusFound, ) } diff --git a/selfservice/flow/registration/handler.go b/selfservice/flow/registration/handler.go index 1a90082478a..7b0574ed398 100644 --- a/selfservice/flow/registration/handler.go +++ b/selfservice/flow/registration/handler.go @@ -5,8 +5,8 @@ import ( "net/url" "github.com/julienschmidt/httprouter" - "github.com/pkg/errors" + "github.com/ory/x/errorsx" "github.com/ory/x/urlx" "github.com/ory/kratos/driver/configuration" @@ -59,7 +59,7 @@ func (h *Handler) NewRegistrationRequest(w http.ResponseWriter, r *http.Request, } if err := h.d.RegistrationExecutor().PreRegistrationHook(w, r, a); err != nil { - if errors.Cause(err) == ErrHookAbortRequest { + if errorsx.Cause(err) == ErrHookAbortRequest { return nil } return err @@ -96,9 +96,9 @@ func (h *Handler) NewRegistrationRequest(w http.ResponseWriter, r *http.Request, // 500: genericError func (h *Handler) initRegistrationRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if err := h.NewRegistrationRequest(w, r, func(a *Request) string { - return urlx.CopyWithQuery(h.c.RegisterURL(), url.Values{"request": {a.ID}}).String() + return urlx.CopyWithQuery(h.c.RegisterURL(), url.Values{"request": {a.ID.String()}}).String() }); err != nil { - h.d.ErrorManager().ForwardError(r.Context(), w, r, err) + h.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } } @@ -122,11 +122,11 @@ func (h *Handler) initRegistrationRequest(w http.ResponseWriter, r *http.Request // 404: genericError // 500: genericError func (h *Handler) fetchRegistrationRequest(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - ar, err := h.d.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), r.URL.Query().Get("request")) + ar, err := h.d.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), x.ParseUUID(r.URL.Query().Get("request"))) if err != nil { h.d.Writer().WriteError(w, r, err) return } - h.d.Writer().Write(w, r, ar.Declassify()) + h.d.Writer().Write(w, r, ar) } diff --git a/selfservice/flow/registration/handler_test.go b/selfservice/flow/registration/handler_test.go index 78ff16c0912..f3b01b09beb 100644 --- a/selfservice/flow/registration/handler_test.go +++ b/selfservice/flow/registration/handler_test.go @@ -24,8 +24,12 @@ import ( "github.com/ory/kratos/x" ) +func init() { + internal.RegisterFakes() +} + func TestEnsureSessionRedirect(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) router := x.NewRouterPublic() reg.RegistrationHandler().RegisterPublicRoutes(router) @@ -60,7 +64,7 @@ func TestEnsureSessionRedirect(t *testing.T) { } func TestRegistrationHandler(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) router := x.NewRouterPublic() reg.RegistrationHandler().RegisterPublicRoutes(router) diff --git a/selfservice/flow/registration/hook.go b/selfservice/flow/registration/hook.go index 026a7bdcfc7..5ac8b1c3fce 100644 --- a/selfservice/flow/registration/hook.go +++ b/selfservice/flow/registration/hook.go @@ -3,8 +3,12 @@ package registration import ( "net/http" + "github.com/ory/x/errorsx" + "github.com/ory/x/sqlcon" + "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" + "github.com/ory/kratos/schema" "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) @@ -56,7 +60,10 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque return err // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which // would imply that the identity has to exist already. - } else if _, err := e.d.IdentityPool().Create(r.Context(), s.Identity); err != nil { + } else if err := e.d.IdentityPool().CreateIdentity(r.Context(), s.Identity); err != nil { + if errorsx.Cause(err) == sqlcon.ErrUniqueViolation { + return schema.NewDuplicateCredentialsError() + } return err } @@ -77,7 +84,7 @@ func (e *HookExecutor) PostRegistrationHook(w http.ResponseWriter, r *http.Reque return err // We're now creating the identity because any of the hooks could trigger a "redirect" or a "session" which // would imply that the identity has to exist already. - } else if _, err := e.d.IdentityPool().Update(r.Context(), s.Identity); err != nil { + } else if err := e.d.IdentityPool().UpdateIdentity(r.Context(), s.Identity); err != nil { return err } diff --git a/selfservice/flow/registration/hook_test.go b/selfservice/flow/registration/hook_test.go index 0ac3225f78e..68e762dba26 100644 --- a/selfservice/flow/registration/hook_test.go +++ b/selfservice/flow/registration/hook_test.go @@ -2,7 +2,6 @@ package registration_test import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -30,7 +29,7 @@ type registrationPostHookMock struct { func (m *registrationPostHookMock) ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { if m.modifyIdentity { i := s.Identity - i.Traits = json.RawMessage(`{"foo":"bar"}"`) + i.Traits = identity.Traits(`{"foo":"bar"}"`) s.UpdateIdentity(i) } return m.err @@ -91,17 +90,17 @@ func TestRegistrationExecutor(t *testing.T) { new(registrationPostHookMock), ®istrationPostHookMock{modifyIdentity: true}, }, - expectTraits: `{"foo":"bar"}"`, + expectTraits: `{"foo":"bar"}`, }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) + conf, reg := internal.NewRegistryDefault(t) viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://stub/registration.schema.json") var i identity.Identity require.NoError(t, faker.FakeData(&i)) i.TraitsSchemaURL = "" - i.Traits = json.RawMessage("{}") + i.Traits = identity.Traits("{}") e := registration.NewHookExecutor(reg, conf) err := e.PostRegistrationHook(nil, &http.Request{}, tc.hooks, nil, &i) @@ -112,7 +111,7 @@ func TestRegistrationExecutor(t *testing.T) { require.NoError(t, err) if tc.expectTraits != "" { - got, err := reg.IdentityPool().Get(context.TODO(), i.ID) + got, err := reg.IdentityPool().GetIdentity(context.TODO(), i.ID) require.NoError(t, err) assert.EqualValues(t, tc.expectTraits, string(got.Traits)) } @@ -134,7 +133,7 @@ func TestRegistrationExecutor(t *testing.T) { }, } { t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) { - conf, _ := internal.NewMemoryRegistry(t) + conf, _ := internal.NewRegistryDefault(t) e := registration.NewHookExecutor(tc.reg, conf) if tc.expectErr == nil { require.NoError(t, e.PreRegistrationHook(nil, nil, nil)) diff --git a/selfservice/flow/registration/persistence.go b/selfservice/flow/registration/persistence.go index 6d01c5b1f5e..bcb53d28ebc 100644 --- a/selfservice/flow/registration/persistence.go +++ b/selfservice/flow/registration/persistence.go @@ -2,75 +2,118 @@ package registration import ( "context" + "encoding/json" "testing" "github.com/bxcodec/faker" + "github.com/gobuffalo/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ory/kratos/identity" + "github.com/ory/kratos/selfservice/form" + "github.com/ory/kratos/x" ) type RequestPersister interface { CreateRegistrationRequest(context.Context, *Request) error - GetRegistrationRequest(ctx context.Context, id string) (*Request, error) - UpdateRegistrationRequest(context.Context, string, identity.CredentialsType, *RequestMethod) error + GetRegistrationRequest(context.Context, uuid.UUID) (*Request, error) + UpdateRegistrationRequest(context.Context, uuid.UUID, identity.CredentialsType, *RequestMethod) error } type RequestPersistenceProvider interface { RegistrationRequestPersister() RequestPersister } -func TestRequestPersister(t *testing.T, p RequestPersister) { - // nbr := func() *Request { - // return &Request{ - // ID: uuid.New().String(), - // IssuedAt: time.Now().UTC().Round(time.Second), - // ExpiresAt: time.Now().Add(time.Hour).UTC().Round(time.Second), - // RequestURL: "https://www.ory.sh/request", - // RequestHeaders: http.Header{"Content-Type": {"application/json"}}, - // // Disable Active as this value is initially empty (NULL). - // // Active: identity.CredentialsTypePassword, - // Methods: map[identity.CredentialsType]*CredentialsRequest{ - // identity.CredentialsTypePassword: { - // Method: identity.CredentialsTypePassword, - // Config: password.NewRequestMethodConfig(), - // }, - // identity.CredentialsTypeOIDC: { - // Method: identity.CredentialsTypeOIDC, - // Config: oidc.NewRequestMethodConfig(), - // }, - // }, - // } - // } - // - // assertUpdated := func(t *testing.T, expected, actual Request) { - // assert.EqualValues(t, identity.CredentialsTypePassword, actual.Active) - // assert.EqualValues(t, "bar", actual.Methods[identity.CredentialsTypeOIDC].Config.(*oidc.RequestMethodConfig).Action) - // assert.EqualValues(t, "foo", actual.Methods[identity.CredentialsTypePassword].Config.(*password.RequestMethodConfig).Action) - // } - - t.Run("case=should error when the registration request does not exist", func(t *testing.T) { - _, err := p.GetRegistrationRequest(context.Background(), "does-not-exist") - require.NoError(t, err) - }) - - t.Run("case=", func(t *testing.T) { - var r Request - require.NoError(t, faker.FakeData(&r)) - t.Logf("%+v", r) - }) - - // r := LoginRequest{Request: nbr()} - // require.NoError(t, p.CreateLoginRequest(context.Background(), &r)) - // - // g, err := p.GetLoginRequest(context.Background(), r.ID) - // require.NoError(t, err) - // assert.EqualValues(t, r, *g) - // - // require.NoError(t, p.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypeOIDC, &oidc.RequestMethod{Action: "bar"})) - // require.NoError(t, p.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypePassword, &password.RequestMethod{Action: "foo"})) - // - // g, err = p.GetLoginRequest(context.Background(), r.ID) - // require.NoError(t, err) - // assertUpdated(t, *r.Request, *g.Request) +func TestRequestPersister(p RequestPersister) func(t *testing.T) { + var clearids = func(r *Request) { + r.ID = uuid.UUID{} + for k := range r.Methods { + r.Methods[k].ID = uuid.UUID{} + } + } + + return func(t *testing.T) { + t.Run("case=should error when the registration request does not exist", func(t *testing.T) { + _, err := p.GetRegistrationRequest(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + var newRequest = func(t *testing.T) *Request { + var r Request + require.NoError(t, faker.FakeData(&r)) + clearids(&r) + + methods := len(r.Methods) + assert.NotZero(t, methods) + + return &r + } + + t.Run("case=should create a new registration request and properly set IDs", func(t *testing.T) { + r := newRequest(t) + methods := len(r.Methods) + err := p.CreateRegistrationRequest(context.Background(), r) + require.NoError(t, err, "%#v", err) + + assert.Nil(t, r.MethodsRaw) + assert.NotEqual(t, uuid.Nil, r.ID) + for _, m := range r.Methods { + assert.NotEqual(t, uuid.Nil, m.ID) + } + assert.Len(t, r.Methods, methods) + }) + + t.Run("case=should create with set ids", func(t *testing.T) { + var r Request + require.NoError(t, faker.FakeData(&r)) + require.NoError(t, p.CreateRegistrationRequest(context.Background(), &r)) + }) + + t.Run("case=should create and fetch a registration request", func(t *testing.T) { + expected := newRequest(t) + err := p.CreateRegistrationRequest(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.GetRegistrationRequest(context.Background(), expected.ID) + require.NoError(t, err) + assert.Empty(t, actual.MethodsRaw) + + assert.EqualValues(t, expected.ID, actual.ID) + x.AssertEqualTime(t, expected.IssuedAt, actual.IssuedAt) + x.AssertEqualTime(t, expected.ExpiresAt, actual.ExpiresAt) + assert.EqualValues(t, expected.RequestURL, actual.RequestURL) + assert.EqualValues(t, expected.Active, actual.Active) + require.Equal(t, len(expected.Methods), len(actual.Methods), "expected:\t%s\nactual:\t%s", expected.Methods, actual.Methods) + }) + + t.Run("case=should update a registration request", func(t *testing.T) { + expected := newRequest(t) + delete(expected.Methods, identity.CredentialsTypeOIDC) + err := p.CreateRegistrationRequest(context.Background(), expected) + require.NoError(t, err) + + actual, err := p.GetRegistrationRequest(context.Background(), expected.ID) + require.NoError(t, err) + assert.Len(t, actual.Methods, 1) + + require.NoError(t, p.UpdateRegistrationRequest(context.Background(), expected.ID, identity.CredentialsTypeOIDC, &RequestMethod{ + Method: identity.CredentialsTypeOIDC, + Config: &RequestMethodConfig{form.NewHTMLForm(string(identity.CredentialsTypeOIDC))}, + })) + + require.NoError(t, p.UpdateRegistrationRequest(context.Background(), expected.ID, identity.CredentialsTypePassword, &RequestMethod{ + Method: identity.CredentialsTypePassword, + Config: &RequestMethodConfig{form.NewHTMLForm(string(identity.CredentialsTypePassword))}, + })) + + actual, err = p.GetRegistrationRequest(context.Background(), expected.ID) + require.NoError(t, err) + require.Len(t, actual.Methods, 2) + + js, _ := json.Marshal(actual.Methods) + assert.Equal(t, string(identity.CredentialsTypePassword), actual.Methods[identity.CredentialsTypePassword].Config.RequestMethodConfigurator.(*form.HTMLForm).Action, "%s", js) + assert.Equal(t, string(identity.CredentialsTypeOIDC), actual.Methods[identity.CredentialsTypeOIDC].Config.RequestMethodConfigurator.(*form.HTMLForm).Action) + }) + } } diff --git a/selfservice/flow/registration/request.go b/selfservice/flow/registration/request.go index 24a4464ae60..bb270e01361 100644 --- a/selfservice/flow/registration/request.go +++ b/selfservice/flow/registration/request.go @@ -4,60 +4,50 @@ import ( "net/http" "time" - "github.com/google/uuid" + "github.com/gobuffalo/pop" + "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/ory/herodot" "github.com/ory/x/urlx" "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/form" + "github.com/ory/kratos/x" ) -// swagger:model registrationRequestMethod -type RequestMethod struct { - // Method contains the request credentials type. - Method identity.CredentialsType `json:"method"` - - // Config is the credential type's config. - Config RequestMethodConfig `json:"config"` -} - -// swagger:model registrationRequestMethodConfig -type RequestMethodConfig interface { - form.ErrorParser - form.CSRFSetter - form.Resetter - form.ValueSetter - form.FieldSetter -} - // swagger:model registrationRequest type Request struct { // ID represents the request's unique ID. When performing the registration flow, this - // represents the id in the registration ui's query parameter: http://registration-ui/?request= - ID string `json:"id"` + // represents the id in the registration ui's query parameter: http:///?request= + ID uuid.UUID `json:"id" faker:"uuid" db:"id" rw:"r"` // ExpiresAt is the time (UTC) when the request expires. If the user still wishes to log in, // a new request has to be initiated. - ExpiresAt time.Time `json:"expires_at"` + ExpiresAt time.Time `json:"expires_at" faker:"time_type" db:"expires_at"` // IssuedAt is the time (UTC) when the request occurred. - IssuedAt time.Time `json:"issued_at"` + IssuedAt time.Time `json:"issued_at" faker:"time_type" db:"issued_at"` // RequestURL is the initial URL that was requested from ORY Kratos. It can be used // to forward information contained in the URL's path or query for example. - RequestURL string `json:"request_url"` + RequestURL string `json:"request_url" db:"request_url"` // Active, if set, contains the registration method that is being used. It is initially // not set. - Active identity.CredentialsType `json:"active,omitempty"` + Active identity.CredentialsType `json:"active,omitempty" db:"active_method"` // Methods contains context for all enabled registration methods. If a registration request has been // processed, but for example the password is incorrect, this will contain error messages. - Methods map[identity.CredentialsType]*RequestMethod `json:"methods" faker:"registration_request_methods"` + Methods map[identity.CredentialsType]*RequestMethod `json:"methods" faker:"registration_request_methods" db:"-"` - RequestHeaders http.Header `json:"-" faker:"http_header"` + // MethodsRaw is a helper struct field for gobuffalo.pop. + MethodsRaw RequestMethodsRaw `json:"-" faker:"-" has_many:"selfservice_registration_request_methods" fk_id:"selfservice_registration_request_id"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` } func NewRequest(exp time.Duration, r *http.Request) *Request { @@ -72,24 +62,47 @@ func NewRequest(exp time.Duration, r *http.Request) *Request { } return &Request{ - ID: uuid.New().String(), - ExpiresAt: time.Now().UTC().Add(exp), - IssuedAt: time.Now().UTC(), - RequestURL: source.String(), - RequestHeaders: r.Header, - Methods: map[identity.CredentialsType]*RequestMethod{}, + ID: x.NewUUID(), + ExpiresAt: time.Now().UTC().Add(exp), + IssuedAt: time.Now().UTC(), + RequestURL: source.String(), + Methods: map[identity.CredentialsType]*RequestMethod{}, + } +} + +func (r *Request) BeforeSave(_ *pop.Connection) error { + r.MethodsRaw = make([]RequestMethod, 0, len(r.Methods)) + for _, m := range r.Methods { + r.MethodsRaw = append(r.MethodsRaw, *m) } + r.Methods = nil + return nil +} + +func (r *Request) AfterCreate(c *pop.Connection) error { + return r.AfterFind(c) +} + +func (r *Request) AfterUpdate(c *pop.Connection) error { + return r.AfterFind(c) +} + +func (r *Request) AfterFind(_ *pop.Connection) error { + r.Methods = make(RequestMethods) + for key := range r.MethodsRaw { + m := r.MethodsRaw[key] // required for pointer dereference + r.Methods[m.Method] = &m + } + r.MethodsRaw = nil + return nil } -// Declassify returns a copy of the Request where all sensitive information -// such as request headers is removed. -func (r *Request) Declassify() *Request { - rr := *r - rr.RequestHeaders = http.Header{} - return &rr +func (r Request) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_registration_requests" } -func (r *Request) GetID() string { +func (r *Request) GetID() uuid.UUID { return r.ID } diff --git a/selfservice/flow/registration/request_method.go b/selfservice/flow/registration/request_method.go new file mode 100644 index 00000000000..0f4cf8ad1c6 --- /dev/null +++ b/selfservice/flow/registration/request_method.go @@ -0,0 +1,85 @@ +package registration + +import ( + "database/sql/driver" + "encoding/json" + "time" + + "github.com/gofrs/uuid" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/persistence/aliases" + "github.com/ory/kratos/selfservice/form" +) + +// swagger:model registrationRequestMethod +type RequestMethod struct { + // Method contains the request credentials type. + Method identity.CredentialsType `json:"method" db:"method"` + + // Config is the credential type's config. + Config *RequestMethodConfig `json:"config" db:"config"` + + // ID is a helper struct field for gobuffalo.pop. + ID uuid.UUID `json:"-" db:"id" rw:"r"` + + // RequestID is a helper struct field for gobuffalo.pop. + RequestID uuid.UUID `json:"-" db:"selfservice_registration_request_id"` + + // Request is a helper struct field for gobuffalo.pop. + Request *Request `json:"-" belongs_to:"selfservice_registration_request" fk_id:"RequestID"` + + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" db:"created_at"` + + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" db:"updated_at"` +} + +func (u RequestMethod) TableName() string { + return "selfservice_registration_request_methods" +} + +type RequestMethodsRaw []RequestMethod // workaround for https://github.com/gobuffalo/pop/pull/478 +type RequestMethods map[identity.CredentialsType]*RequestMethod + +func (u RequestMethods) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_registration_request_methods" +} + +func (u RequestMethodsRaw) TableName() string { + // This must be stay a value receiver, using a pointer receiver will cause issues with pop. + return "selfservice_registration_request_methods" +} + +// swagger:ignore +type RequestMethodConfigurator interface { + form.ErrorParser + form.FieldSetter + form.ValueSetter + form.Resetter + form.CSRFSetter +} + +// swagger:model registrationRequestMethodConfig +type RequestMethodConfig struct { + RequestMethodConfigurator +} + +func (c *RequestMethodConfig) Scan(value interface{}) error { + return aliases.JSONScan(c, value) +} + +func (c *RequestMethodConfig) Value() (driver.Value, error) { + return aliases.JSONValue(c) +} + +func (c *RequestMethodConfig) UnmarshalJSON(data []byte) error { + c.RequestMethodConfigurator = new(form.HTMLForm) + return json.Unmarshal(data, c.RequestMethodConfigurator) +} + +func (c *RequestMethodConfig) MarshalJSON() ([]byte, error) { + return json.Marshal(c.RequestMethodConfigurator) +} diff --git a/selfservice/flow/registration/request_test.go b/selfservice/flow/registration/request_test.go index d3718dedcc0..0abd3331b2f 100644 --- a/selfservice/flow/registration/request_test.go +++ b/selfservice/flow/registration/request_test.go @@ -8,13 +8,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/flow/registration" + "github.com/ory/kratos/x" ) func TestFakeRequestData(t *testing.T) { - internal.RegisterFakes() - var r registration.Request require.NoError(t, faker.FakeData(&r)) @@ -28,18 +26,11 @@ func TestFakeRequestData(t *testing.T) { assert.NotEmpty(t, m.Method) assert.NotEmpty(t, m.Config) } - - assert.NotEmpty(t, r.RequestHeaders) - for k, v := range r.RequestHeaders { - assert.NotEmpty(t, k) - assert.NotEmpty(t, v) - } } func TestRequest(t *testing.T) { - r := ®istration.Request{ID: "request", RequestHeaders: map[string][]string{"foo": {"bar"}}} + r := ®istration.Request{ID: x.NewUUID()} assert.Equal(t, r.ID, r.GetID()) - assert.Empty(t, r.Declassify().RequestHeaders) t.Run("case=expired", func(t *testing.T) { for _, tc := range []struct { diff --git a/selfservice/form/html_form.go b/selfservice/form/html_form.go index d59c997b7e7..241e0df1da2 100644 --- a/selfservice/form/html_form.go +++ b/selfservice/form/html_form.go @@ -1,18 +1,21 @@ package form import ( + "database/sql/driver" "encoding/json" "net/http" "sync" - "github.com/pkg/errors" "github.com/santhosh-tekuri/jsonschema/v2" + "github.com/ory/x/errorsx" + "github.com/ory/x/decoderx" "github.com/ory/x/jsonschemax" "github.com/ory/x/jsonx" "github.com/ory/x/stringslice" + "github.com/ory/kratos/persistence/aliases" "github.com/ory/kratos/schema" ) @@ -28,7 +31,7 @@ var ( // // swagger:model form type HTMLForm struct { - sync.RWMutex + *sync.RWMutex // Action should be used as the form action URL (
). Action string `json:"action"` @@ -36,7 +39,7 @@ type HTMLForm struct { // Method is the form method (e.g. POST) Method string `json:"method"` - // Fields contains the form fields asdfasdffasd + // Fields contains the form fields. Fields Fields `json:"fields"` // Errors contains all form errors. These will be duplicates of the individual field errors. @@ -46,9 +49,10 @@ type HTMLForm struct { // NewHTMLForm returns an empty container. func NewHTMLForm(action string) *HTMLForm { return &HTMLForm{ - Action: action, - Method: "POST", - Fields: Fields{}, + RWMutex: new(sync.RWMutex), + Action: action, + Method: "POST", + Fields: Fields{}, } } @@ -124,7 +128,7 @@ func (c *HTMLForm) Reset() { // This method DOES NOT touch the values of the form fields, only its errors. func (c *HTMLForm) ParseError(err error) error { c.defaults() - switch e := errors.Cause(err).(type) { + switch e := errorsx.Cause(err).(type) { case richError: if e.StatusCode() == http.StatusBadRequest { c.AddError(&Error{Message: e.Reason()}) @@ -269,6 +273,13 @@ func (c *HTMLForm) AddError(err *Error, names ...string) { } } +func (c *HTMLForm) Scan(value interface{}) error { + return aliases.JSONScan(c, value) +} +func (c *HTMLForm) Value() (driver.Value, error) { + return aliases.JSONValue(c) +} + func (c *HTMLForm) defaults() { c.Lock() defer c.Unlock() diff --git a/selfservice/hook/redirector_test.go b/selfservice/hook/redirector_test.go index 885e9ca77c0..7efbce847bf 100644 --- a/selfservice/hook/redirector_test.go +++ b/selfservice/hook/redirector_test.go @@ -7,10 +7,11 @@ import ( "net/url" "testing" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/ory/x/errorsx" + "github.com/ory/viper" "github.com/ory/herodot" @@ -53,7 +54,7 @@ func TestRedirector(t *testing.T) { var assert = func(t *testing.T, tc testCase, w *httptest.ResponseRecorder, err error) { if tc.expectErr != "" { require.Error(t, err) - assert.Contains(t, errors.Cause(err).(*herodot.DefaultError).Reason(), tc.expectErr) + assert.Contains(t, errorsx.Cause(err).(*herodot.DefaultError).Reason(), tc.expectErr) return } require.NoError(t, err) diff --git a/selfservice/hook/session_issuer.go b/selfservice/hook/session_issuer.go index 08298e3d116..52f911faf6f 100644 --- a/selfservice/hook/session_issuer.go +++ b/selfservice/hook/session_issuer.go @@ -13,6 +13,7 @@ var _ registration.PostHookExecutor = new(SessionIssuer) type sessionIssuerDependencies interface { session.ManagementProvider + session.PersistenceProvider } type SessionIssuer struct { @@ -24,14 +25,14 @@ func NewSessionIssuer(r sessionIssuerDependencies) *SessionIssuer { } func (e *SessionIssuer) ExecuteRegistrationPostHook(w http.ResponseWriter, r *http.Request, a *registration.Request, s *session.Session) error { - if err := e.r.SessionManager().Create(r.Context(), s); err != nil { + if err := e.r.SessionPersister().CreateSession(r.Context(), s); err != nil { return err } return e.r.SessionManager().SaveToRequest(r.Context(), s, w, r) } func (e *SessionIssuer) ExecuteLoginPostHook(w http.ResponseWriter, r *http.Request, a *login.Request, s *session.Session) error { - if err := e.r.SessionManager().Create(r.Context(), s); err != nil { + if err := e.r.SessionPersister().CreateSession(r.Context(), s); err != nil { return err } return e.r.SessionManager().SaveToRequest(r.Context(), s, w, r) diff --git a/selfservice/hook/session_issuer_test.go b/selfservice/hook/session_issuer_test.go index 8fe3aaec65e..869c5499a52 100644 --- a/selfservice/hook/session_issuer_test.go +++ b/selfservice/hook/session_issuer_test.go @@ -6,7 +6,6 @@ import ( "net/http/httptest" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,10 +16,11 @@ import ( "github.com/ory/kratos/internal" "github.com/ory/kratos/selfservice/hook" "github.com/ory/kratos/session" + "github.com/ory/kratos/x" ) func TestSessionIssuer(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) viper.Set(configuration.ViperKeyURLsSelfPublic, "http://localhost/") viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/stub.schema.json") @@ -29,29 +29,27 @@ func TestSessionIssuer(t *testing.T) { t.Run("method=sign-in", func(t *testing.T) { w := httptest.NewRecorder() - sid := uuid.New().String() + sid := x.NewUUID() i := identity.NewIdentity("") - i, err := reg.IdentityPool().Create(context.Background(), i) - require.NoError(t, err) - require.NoError(t, h.ExecuteLoginPostHook(w, &r, nil, &session.Session{SID: sid, Identity: i})) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, h.ExecuteLoginPostHook(w, &r, nil, &session.Session{ID: sid, Identity: i})) - got, err := reg.SessionManager().Get(context.Background(), sid) + got, err := reg.SessionPersister().GetSession(context.Background(), sid) require.NoError(t, err) - assert.Equal(t, sid, got.SID) + assert.Equal(t, sid, got.ID) }) t.Run("method=sign-up", func(t *testing.T) { w := httptest.NewRecorder() - sid := uuid.New().String() + sid := x.NewUUID() i := identity.NewIdentity("") - i, err := reg.IdentityPool().Create(context.Background(), i) - require.NoError(t, err) - require.NoError(t, h.ExecuteRegistrationPostHook(w, &r, nil, &session.Session{SID: sid, Identity: i})) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), i)) + require.NoError(t, h.ExecuteRegistrationPostHook(w, &r, nil, &session.Session{ID: sid, Identity: i})) - got, err := reg.SessionManager().Get(context.Background(), sid) + got, err := reg.SessionPersister().GetSession(context.Background(), sid) require.NoError(t, err) - assert.Equal(t, sid, got.SID) + assert.Equal(t, sid, got.ID) }) } diff --git a/selfservice/persistence/ephermal.go b/selfservice/persistence/ephermal.go deleted file mode 100644 index a66d27c20f9..00000000000 --- a/selfservice/persistence/ephermal.go +++ /dev/null @@ -1,149 +0,0 @@ -package persistence - -import ( - "context" - "sync" - - "github.com/pkg/errors" - - "github.com/ory/herodot" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/profile" - "github.com/ory/kratos/selfservice/flow/registration" -) - -var _ registration.RequestPersister = new(RequestManagerMemory) -var _ login.RequestPersister = new(RequestManagerMemory) -var _ profile.RequestPersister = new(RequestManagerMemory) - -type RequestManagerMemory struct { - sync.RWMutex - sir map[string]login.Request - sur map[string]registration.Request - pr map[string]profile.Request -} - -func NewRequestManagerMemory() *RequestManagerMemory { - return &RequestManagerMemory{ - sir: make(map[string]login.Request), - sur: make(map[string]registration.Request), - pr: make(map[string]profile.Request), - } -} - -func (m *RequestManagerMemory) cr(r interface{}) error { - m.Lock() - defer m.Unlock() - switch t := r.(type) { - case *login.Request: - m.sir[t.ID] = *t - case *registration.Request: - m.sur[t.ID] = *t - case *profile.Request: - m.pr[t.ID] = *t - default: - panic("Unknown type") - } - return nil -} - -func (m *RequestManagerMemory) CreateLoginRequest(ctx context.Context, r *login.Request) error { - return m.cr(r) -} - -func (m *RequestManagerMemory) CreateRegistrationRequest(ctx context.Context, r *registration.Request) error { - return m.cr(r) -} - -func (m *RequestManagerMemory) GetLoginRequest(ctx context.Context, id string) (*login.Request, error) { - m.RLock() - defer m.RUnlock() - if r, ok := m.sir[id]; ok { - return &r, nil - } - - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find request: %s", id)) -} - -func (m *RequestManagerMemory) GetRegistrationRequest(ctx context.Context, id string) (*registration.Request, error) { - m.RLock() - defer m.RUnlock() - if r, ok := m.sur[id]; ok { - return &r, nil - } - - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find request: %s", id)) -} - -func (m *RequestManagerMemory) UpdateRegistrationRequest(ctx context.Context, id string, t identity.CredentialsType, c *registration.RequestMethod) error { - r, err := m.GetRegistrationRequest(ctx, id) - if err != nil { - return err - } - - m.Lock() - defer m.Unlock() - - me, ok := r.Methods[t] - if !ok { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Expected registration request "%s" to have credentials type "%s", indicating an internal error.`, id, t)) - } - - me.Config = c.Config - r.Active = t - r.Methods[t] = me - m.sur[id] = *r - - return nil -} - -func (m *RequestManagerMemory) UpdateLoginRequest(ctx context.Context, id string, t identity.CredentialsType, c *login.RequestMethod) error { - r, err := m.GetLoginRequest(ctx, id) - if err != nil { - return err - } - - m.Lock() - defer m.Unlock() - - me, ok := r.Methods[t] - if !ok { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Expected login request "%s" to have credentials type "%s", indicating an internal error.`, id, t)) - } - - me.Config = c.Config - r.Active = t - r.Methods[t] = me - m.sir[id] = *r - - return nil -} - -func (m *RequestManagerMemory) CreateProfileRequest(ctx context.Context, r *profile.Request) error { - return m.cr(r) -} - -func (m *RequestManagerMemory) GetProfileRequest(ctx context.Context, id string) (*profile.Request, error) { - m.RLock() - defer m.RUnlock() - if r, ok := m.pr[id]; ok { - return &r, nil - } - - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find request: %s", id)) -} - -func (m *RequestManagerMemory) UpdateProfileRequest(ctx context.Context, id string, request *profile.Request) error { - m.Lock() - defer m.Unlock() - - if _, ok := m.pr[id]; !ok { - return errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find request: %s", id)) - } - - request.ID = id - m.pr[id] = *request - return nil -} diff --git a/selfservice/persistence/persister.go b/selfservice/persistence/persister.go deleted file mode 100644 index 7c80c2828ba..00000000000 --- a/selfservice/persistence/persister.go +++ /dev/null @@ -1,13 +0,0 @@ -package persistence - -import ( - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/profile" - "github.com/ory/kratos/selfservice/flow/registration" -) - -type RequestPersister interface { - registration.RequestPersister - login.RequestPersister - profile.RequestPersister -} diff --git a/selfservice/persistence/postgresql.go.bak b/selfservice/persistence/postgresql.go.bak deleted file mode 100644 index 8b7ec7236fa..00000000000 --- a/selfservice/persistence/postgresql.go.bak +++ /dev/null @@ -1,220 +0,0 @@ -package persistence - -import ( - "bytes" - "context" - "database/sql" - "encoding/json" - "fmt" - "net/http" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/ory/x/jsonx" - - "github.com/ory/x/sqlcon" - "github.com/ory/x/sqlxx" - - "github.com/ory/herodot" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/registration" - - "github.com/ory/kratos/selfservice/flow/profile" -) - -var _ RequestPersister = new(RequestManagerSQL) - -const requestSQLTableName = "self_service_request" - -type RequestManagerSQL struct { - db *sqlx.DB - rmf map[identity.CredentialsType]func() -} - -type requestSQL struct { - ID string `db:"id"` - IssuedAt time.Time `db:"issued_at"` - ExpiresAt time.Time `db:"expires_at"` - RequestURL string `db:"request_url"` - RequestHeaders json.RawMessage `db:"request_headers"` - Active sql.NullString `db:"active"` - Methods json.RawMessage `db:"methods"` - Kind string `db:"kind"` -} - -// func newRequestSQL(r *Request, kind string) (*requestSQL, error) { -// var requestHeaders bytes.Buffer -// var methods bytes.Buffer -// -// if err := json.NewEncoder(&requestHeaders).Encode(r.RequestHeaders); err != nil { -// return nil, errors.WithStack(err) -// } -// -// if err := json.NewEncoder(&methods).Encode(r.Methods); err != nil { -// return nil, errors.WithStack(err) -// } -// -// return &requestSQL{ -// ID: r.ID, -// IssuedAt: r.IssuedAt, -// ExpiresAt: r.ExpiresAt, -// RequestURL: r.RequestURL, -// RequestHeaders: requestHeaders.Bytes(), -// Active: sql.NullString{String: string(r.Active), Valid: len(r.Active) > 0}, -// Methods: methods.Bytes(), -// Kind: kind, -// }, nil -// } - -func NewRequestManagerSQL(db *sqlx.DB) *RequestManagerSQL { - return &RequestManagerSQL{db: db} -} - -func (m *RequestManagerSQL) CreateLoginRequest(ctx context.Context, r *login.Request) error { - return m.cr(ctx, r.Request, "login") -} - -func (m *RequestManagerSQL) CreateRegistrationRequest(ctx context.Context, r *registration.Request) error { - return m.cr(ctx, r.Request, "registration") -} - -func (m *RequestManagerSQL) GetLoginRequest(ctx context.Context, id string) (*login.Request, error) { - r, err := m.gr(ctx, id, "login") - if err != nil { - return nil, err - } - - return &login.Request{Request: r}, nil -} - -func (m *RequestManagerSQL) GetRegistrationRequest(ctx context.Context, id string) (*registration.Request, error) { - r, err := m.gr(ctx, id, "registration") - if err != nil { - return nil, err - } - - return ®istration.Request{Request: r}, nil -} - -func (m *RequestManagerSQL) UpdateRegistrationRequest(ctx context.Context, id string, t identity.CredentialsType, c FormProvider) error { - r, err := m.GetRegistrationRequest(ctx, id) - if err != nil { - return err - } - - return m.ur(ctx, r.Request, t, c, "registration") -} - -func (m *RequestManagerSQL) UpdateLoginRequest(ctx context.Context, id string, t identity.CredentialsType, c FormProvider) error { - r, err := m.GetLoginRequest(ctx, id) - if err != nil { - return err - } - - return m.ur(ctx, r.Request, t, c, "login") -} - -func (m *RequestManagerSQL) ur(ctx context.Context, r *Request, t identity.CredentialsType, c FormProvider, kind string) error { - me, ok := r.Methods[t] - if !ok { - return errors.WithStack(herodot.ErrInternalServerError.WithReasonf(`Expected %s request "%s" to have credentials type "%s", indicating an internal error.`, kind, r.ID, t)) - } - - me.Config = c - r.Active = t - r.Methods[t] = me - - update, err := newRequestSQL(r, kind) - if err != nil { - return err - } - - query := fmt.Sprintf("UPDATE %s SET %s", requestSQLTableName, sqlxx.NamedUpdateArguments(update, "id", "issued_at", "expires_at", "request_url", "headers")) - if _, err := m.db.NamedExecContext(ctx, m.db.Rebind(query), update); err != nil { - return sqlcon.HandleError(err) - } - - return nil -} -func (m *RequestManagerSQL) cr(ctx context.Context, r *Request, kind string) error { - insert, err := newRequestSQL(r, kind) - if err != nil { - return err - } - - columns, arguments := sqlxx.NamedInsertArguments(insert) - query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", requestSQLTableName, columns, arguments) - if _, err := m.db.NamedExecContext(ctx, m.db.Rebind(query), insert); err != nil { - return sqlcon.HandleError(err) - } - - return nil -} - -func (m *RequestManagerSQL) gr(ctx context.Context, id string, kind string) (*Request, error) { - var r requestSQL - - columns, _ := sqlxx.NamedInsertArguments(r, "pk") - query := fmt.Sprintf("SELECT %s FROM %s WHERE id=? AND kind=?", columns, requestSQLTableName) - if err := sqlcon.HandleError(m.db.GetContext(ctx, &r, m.db.Rebind(query), id, kind)); err != nil { - if errors.Cause(err) == sqlcon.ErrNoRows { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("%s", err)) - } - return nil, err - } - - var header http.Header - if err := jsonx.NewStrictDecoder(bytes.NewBuffer(r.RequestHeaders)).Decode(&header); err != nil { - return nil, errors.WithStack(err) - } - - var methodsRaw map[string]json.RawMessage - if err := jsonx.NewStrictDecoder(bytes.NewBuffer(r.Methods)).Decode(&methodsRaw); err != nil { - return nil, errors.WithStack(err) - } - - methods := map[identity.CredentialsType]*CredentialsRequest{} - for method, raw := range methodsRaw { - ct := identity.CredentialsType(method) - var config CredentialsRequest - prototype, found := m.rmf[ct] - if !found { - panic(fmt.Sprintf("unknown credentials type: %s", method)) - } - - config.Config = prototype() - if err := jsonx.NewStrictDecoder(bytes.NewBuffer(raw)).Decode(&config); err != nil { - return nil, errors.WithStack(err) - } - methods[ct] = &config - } - - var active identity.CredentialsType - if r.Active.Valid { - active = identity.CredentialsType(r.Active.String) - } - - return &Request{ - ID: r.ID, - IssuedAt: r.IssuedAt.UTC(), - ExpiresAt: r.ExpiresAt.UTC(), - RequestURL: r.RequestURL, - RequestHeaders: header, - Active: active, - Methods: methods, - }, nil -} - -func (m *RequestManagerSQL) CreateProfileRequest(context.Context, *profile.Request) error { - panic("") -} -func (m *RequestManagerSQL) GetProfileRequest(ctx context.Context, id string) (*profile.Request, error) { - panic("") -} -func (m *RequestManagerSQL) UpdateProfileRequest(context.Context, string, FormProvider) error { - panic("") -} diff --git a/selfservice/persistence/prototypes.go b/selfservice/persistence/prototypes.go deleted file mode 100644 index 84a0f0548b0..00000000000 --- a/selfservice/persistence/prototypes.go +++ /dev/null @@ -1,56 +0,0 @@ -package persistence - -import ( - "fmt" - "sync" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/selfservice/flow/login" - "github.com/ory/kratos/selfservice/flow/registration" -) - -var loginRequestPrototypeFactories = map[identity.CredentialsType]func() login.RequestMethodConfig{} -var registrationRequestPrototypeFactories = map[identity.CredentialsType]func() registration.RequestMethodConfig{} -var pl sync.RWMutex - -// RegisterRegistrationRequestPrototype a login.RequestMethodConfig prototype for a specific identity.CredentialsType. -func RegisterLoginRequestPrototypeFactory(t identity.CredentialsType, f func() login.RequestMethodConfig) { - pl.Lock() - defer pl.Unlock() - - loginRequestPrototypeFactories[t] = f -} - -// RegisterRegistrationRequestPrototype a registration.RequestMethodConfig prototype for a specific identity.CredentialsType. -func RegisterRegistrationRequestPrototypeFactory(t identity.CredentialsType, f func() registration.RequestMethodConfig) { - pl.Lock() - defer pl.Unlock() - - registrationRequestPrototypeFactories[t] = f -} - -// registrationRequestMethodConfigFor returns the registration.RequestMethodConfig for the given identity.CredentialsType. -func registrationRequestMethodConfigFor(t identity.CredentialsType) registration.RequestMethodConfig { - pl.RLock() - defer pl.RUnlock() - - f, ok := registrationRequestPrototypeFactories[t] - if !ok { - panic(fmt.Sprintf("registration.RequestMethodConfig for identity.CredentialsType (%s) was not registered", t)) - } - - return f() -} - -// loginRequestMethodConfigFor returns the login.RequestMethodConfig for the given identity.CredentialsType. -func loginRequestMethodConfigFor(t identity.CredentialsType) login.RequestMethodConfig { - pl.RLock() - defer pl.RUnlock() - - f, ok := loginRequestPrototypeFactories[t] - if !ok { - panic(fmt.Sprintf("login.RequestMethodConfig for identity.CredentialsType (%s) was not registered", t)) - } - - return f() -} diff --git a/selfservice/persistence/prototypes_test.go.bak b/selfservice/persistence/prototypes_test.go.bak deleted file mode 100644 index dc7cf831f60..00000000000 --- a/selfservice/persistence/prototypes_test.go.bak +++ /dev/null @@ -1 +0,0 @@ -package persistence diff --git a/selfservice/persistence/request_manager_test.go.bak b/selfservice/persistence/request_manager_test.go.bak deleted file mode 100644 index fddd6ee2666..00000000000 --- a/selfservice/persistence/request_manager_test.go.bak +++ /dev/null @@ -1,110 +0,0 @@ -package persistence_test - -import ( - "context" - "fmt" - "net/http" - "sync" - "testing" - "time" - - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/x/sqlcon/dockertest" - - "github.com/ory/kratos/identity" - "github.com/ory/kratos/internal" - "github.com/ory/kratos/selfservice/persistence" - "github.com/ory/kratos/selfservice/strategy/oidc" - "github.com/ory/kratos/selfservice/strategy/password" -) - -func TestRequestManagerMemory(t *testing.T) { - managers := map[string]persistence.RequestPersister{ - "memory": persistence.NewRequestManagerMemory(), - } - - if !testing.Short() { - var l sync.Mutex - dockertest.Parallel([]func(){ - func() { - db, err := dockertest.ConnectToTestPostgreSQL() - require.NoError(t, err) - - _, reg := internal.NewRegistrySQL(t, db) - manager := reg.LoginRequestManager().(*persistence.RequestManagerSQL) - - l.Lock() - managers["postgres"] = manager - l.Unlock() - }, - }) - } - - nbr := func() *Request { - return &Request{ - ID: uuid.New().String(), - IssuedAt: time.Now().UTC().Round(time.Second), - ExpiresAt: time.Now().Add(time.Hour).UTC().Round(time.Second), - RequestURL: "https://www.ory.sh/request", - RequestHeaders: http.Header{"Content-Type": {"application/json"}}, - // Disable Active as this value is initially empty (NULL). - // Active: identity.CredentialsTypePassword, - Methods: map[identity.CredentialsType]*CredentialsRequest{ - identity.CredentialsTypePassword: { - Method: identity.CredentialsTypePassword, - Config: password.NewRequestMethodConfig(), - }, - identity.CredentialsTypeOIDC: { - Method: identity.CredentialsTypeOIDC, - Config: oidc.NewRequestMethodConfig(), - }, - }, - } - } - - assertUpdated := func(t *testing.T, expected, actual Request) { - assert.EqualValues(t, identity.CredentialsTypePassword, actual.Active) - assert.EqualValues(t, "bar", actual.Methods[identity.CredentialsTypeOIDC].Config.(*oidc.RequestMethod).Action) - assert.EqualValues(t, "foo", actual.Methods[identity.CredentialsTypePassword].Config.(*password.RequestMethodConfig).Action) - } - - for name, m := range managers { - t.Run(fmt.Sprintf("manager=%s", name), func(t *testing.T) { - - t.Run("suite=login", func(t *testing.T) { - r := LoginRequest{Request: nbr()} - require.NoError(t, m.CreateLoginRequest(context.Background(), &r)) - - g, err := m.GetLoginRequest(context.Background(), r.ID) - require.NoError(t, err) - assert.EqualValues(t, r, *g) - - require.NoError(t, m.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypeOIDC, &oidc.RequestMethod{Action: "bar"})) - require.NoError(t, m.UpdateLoginRequest(context.Background(), r.ID, identity.CredentialsTypePassword, &password.RequestMethodConfig{Action: "foo"})) - - g, err = m.GetLoginRequest(context.Background(), r.ID) - require.NoError(t, err) - assertUpdated(t, *r.Request, *g.Request) - }) - - t.Run("suite=registration", func(t *testing.T) { - r := RegistrationRequest{Request: nbr()} - - require.NoError(t, m.CreateRegistrationRequest(context.Background(), &r)) - g, err := m.GetRegistrationRequest(context.Background(), r.ID) - require.NoError(t, err) - assert.EqualValues(t, r, *g) - - require.NoError(t, m.UpdateRegistrationRequest(context.Background(), r.ID, identity.CredentialsTypeOIDC, &oidc.RequestMethod{Action: "bar"})) - require.NoError(t, m.UpdateRegistrationRequest(context.Background(), r.ID, identity.CredentialsTypePassword, &password.RequestMethodConfig{Action: "foo"})) - - g, err = m.GetRegistrationRequest(context.Background(), r.ID) - require.NoError(t, err) - assertUpdated(t, *r.Request, *g.Request) - }) - }) - } -} diff --git a/selfservice/strategy/oidc/provider_config_test.go b/selfservice/strategy/oidc/provider_config_test.go index d8bbdebc9d8..0fd687dba74 100644 --- a/selfservice/strategy/oidc/provider_config_test.go +++ b/selfservice/strategy/oidc/provider_config_test.go @@ -16,7 +16,7 @@ import ( ) func TestConfig(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) + conf, reg := internal.NewRegistryDefault(t) viper.Set(configuration.ViperKeySelfServiceStrategyConfig+"."+string(identity.CredentialsTypeOIDC), json.RawMessage(`{"config":{"providers": [{"provider": "generic"}]}}`)) s := oidc.NewStrategy(reg, conf) diff --git a/selfservice/strategy/oidc/strategy.go b/selfservice/strategy/oidc/strategy.go index 981008fd0d7..8323bb06780 100644 --- a/selfservice/strategy/oidc/strategy.go +++ b/selfservice/strategy/oidc/strategy.go @@ -9,11 +9,13 @@ import ( "net/url" "strings" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/justinas/nosurf" "github.com/pkg/errors" + "github.com/ory/x/errorsx" + "github.com/ory/x/jsonx" "github.com/ory/gojsonschema" @@ -83,8 +85,6 @@ type dependencies interface { type Strategy struct { c configuration.Provider d dependencies - - dec *x.BodyDecoder validator *schema.Validator cg form.CSRFGenerator } @@ -124,7 +124,6 @@ func NewStrategy( d: d, cg: nosurf.Token, validator: schema.NewValidator(), - dec: x.NewBodyDecoder(), } } @@ -141,7 +140,7 @@ func (s *Strategy) LoginStrategyID() identity.CredentialsType { } func (s *Strategy) handleAuth(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { - var rid = ps.ByName("request") + rid := x.ParseUUID(ps.ByName("request")) if err := r.ParseForm(); err != nil { s.handleError(w, r, rid, nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error()))) @@ -174,11 +173,11 @@ func (s *Strategy) handleAuth(w http.ResponseWriter, r *http.Request, ps httprou return } - state := uuid.New().String() + state := x.NewUUID().String() // Any data that is posted to this endpoint will be used to fill out missing data from the oidc provider. if err := x.SessionPersistValues(w, r, s.d.CookieManager(), sessionName, map[string]interface{}{ sessionKeyState: state, - sessionRequestID: rid, + sessionRequestID: rid.String(), sessionFormState: r.PostForm.Encode(), }); err != nil { s.handleError(w, r, rid, nil, err) @@ -188,8 +187,8 @@ func (s *Strategy) handleAuth(w http.ResponseWriter, r *http.Request, ps httprou http.Redirect(w, r, config.AuthCodeURL(state), http.StatusFound) } -func (s *Strategy) validateRequest(ctx context.Context, rid string) (request, error) { - if rid == "" { +func (s *Strategy) validateRequest(ctx context.Context, rid uuid.UUID) (request, error) { + if x.IsZeroUUID(rid) { return nil, errors.WithStack(herodot.ErrBadRequest.WithReason("The session cookie contains invalid values and the request could not be executed. Please try again.")) } @@ -222,7 +221,7 @@ func (s *Strategy) validateCallback(r *http.Request) (request, error) { return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf(`Unable to complete OpenID Connect flow because the query state parameter does not match the state parameter from the session cookie.`)) } - ar, err := s.validateRequest(r.Context(), x.SessionGetStringOr(r, s.d.CookieManager(), sessionName, sessionRequestID, "")) + ar, err := s.validateRequest(r.Context(), x.ParseUUID(x.SessionGetStringOr(r, s.d.CookieManager(), sessionName, sessionRequestID, ""))) if err != nil { return nil, err } @@ -249,7 +248,7 @@ func (s *Strategy) handleCallback(w http.ResponseWriter, r *http.Request, ps htt if ar != nil { s.handleError(w, r, ar.GetID(), nil, err) } else { - s.handleError(w, r, "", nil, err) + s.handleError(w, r, x.EmptyUUID, nil, err) } return } @@ -294,11 +293,11 @@ func uid(provider, subject string) string { return fmt.Sprintf("%s:%s", provider, subject) } -func (s *Strategy) authURL(request, provider string) string { +func (s *Strategy) authURL(request uuid.UUID, provider string) string { u := urlx.AppendPaths( urlx.Copy(s.c.SelfPublicURL()), strings.Replace( - AuthPath, ":request", request, 1, + AuthPath, ":request", request.String(), 1, ), ) @@ -312,7 +311,7 @@ func (s *Strategy) authURL(request, provider string) string { func (s *Strategy) processLogin(w http.ResponseWriter, r *http.Request, a *login.Request, claims *Claims, provider Provider) { i, c, err := s.d.IdentityPool().FindByCredentialsIdentifier(r.Context(), identity.CredentialsTypeOIDC, uid(provider.Config().ID, claims.Subject)) if err != nil { - if errors.Cause(err).Error() == herodot.ErrNotFound.Error() { + if errorsx.Cause(err).Error() == herodot.ErrNotFound.Error() { // If no account was found we're "manually" creating a new registration request and redirecting the browser // to that endpoint. @@ -408,14 +407,14 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a traits, err := merge( x.SessionGetStringOr(r, s.d.CookieManager(), sessionName, sessionFormState, ""), - i.Traits, option, + json.RawMessage(i.Traits), option, ) if err != nil { s.handleError(w, r, a.GetID(), nil, err) return } - i.Traits = traits + i.Traits = identity.Traits(traits) // Validate the identity itself if err := s.d.IdentityValidator().Validate(i); err != nil { @@ -433,7 +432,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a } i.SetCredentials(s.RegistrationStrategyID(), identity.Credentials{ - ID: s.RegistrationStrategyID(), + Type: s.RegistrationStrategyID(), Identifiers: []string{uid(provider.Config().ID, claims.Subject)}, Config: b.Bytes(), }) @@ -460,7 +459,7 @@ func (s *Strategy) processRegistration(w http.ResponseWriter, r *http.Request, a // return nil // } -func (s *Strategy) populateMethod(r *http.Request, request string) (*RequestMethod, error) { +func (s *Strategy) populateMethod(r *http.Request, request uuid.UUID) (*RequestMethod, error) { conf, err := s.Config() if err != nil { return nil, err @@ -479,7 +478,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, sr *login.Request) error } sr.Methods[identity.CredentialsTypeOIDC] = &login.RequestMethod{ Method: identity.CredentialsTypeOIDC, - Config: config, + Config: &login.RequestMethodConfig{RequestMethodConfigurator: config}, } return nil } @@ -491,7 +490,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, sr *registration. } sr.Methods[identity.CredentialsTypeOIDC] = ®istration.RequestMethod{ Method: identity.CredentialsTypeOIDC, - Config: config, + Config: ®istration.RequestMethodConfig{RequestMethodConfigurator: config}, } return nil } @@ -520,9 +519,9 @@ func (s *Strategy) provider(id string) (Provider, error) { } } -func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, rid string, traits json.RawMessage, err error) { - if rid == "" { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) +func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, rid uuid.UUID, traits json.RawMessage, err error) { + if x.IsZeroUUID(rid) { + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } @@ -554,5 +553,5 @@ func (s *Strategy) handleError(w http.ResponseWriter, r *http.Request, rid strin return } - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) } diff --git a/selfservice/strategy/oidc/strategy_helper_test.go b/selfservice/strategy/oidc/strategy_helper_test.go index f38bbf7ae9f..773017150a3 100644 --- a/selfservice/strategy/oidc/strategy_helper_test.go +++ b/selfservice/strategy/oidc/strategy_helper_test.go @@ -24,7 +24,7 @@ import ( func newErrTs(t *testing.T, reg driver.Registry) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e, err := reg.ErrorManager().Read(r.Context(), r.URL.Query().Get("error")) + e, err := reg.SelfServiceErrorPersister().Read(r.Context(), x.ParseUUID(r.URL.Query().Get("error"))) require.NoError(t, err) reg.Writer().Write(w, r, e) })) diff --git a/selfservice/strategy/oidc/strategy_test.go b/selfservice/strategy/oidc/strategy_test.go index df0dae3e0ad..e83d00213fc 100644 --- a/selfservice/strategy/oidc/strategy_test.go +++ b/selfservice/strategy/oidc/strategy_test.go @@ -15,7 +15,7 @@ import ( "testing" "time" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -115,7 +115,7 @@ func TestStrategy(t *testing.T) { remoteAdmin = "http://127.0.0.1:" + hydra.GetPort("4445/tcp") } - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) for _, strategy := range reg.LoginStrategies() { // We need to replace the password strategy token generator because it is being used by the error handler... strategy.(withTokenGenerator).WithTokenGenerator(func(r *http.Request) string { @@ -138,9 +138,9 @@ func TestStrategy(t *testing.T) { var e interface{} var err error if r.URL.Path == "/login" { - e, err = reg.LoginRequestPersister().GetLoginRequest(r.Context(), r.URL.Query().Get("request")) + e, err = reg.LoginRequestPersister().GetLoginRequest(r.Context(), x.ParseUUID(r.URL.Query().Get("request"))) } else if r.URL.Path == "/registration" { - e, err = reg.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), r.URL.Query().Get("request")) + e, err = reg.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), x.ParseUUID(r.URL.Query().Get("request"))) } require.NoError(t, err) @@ -218,9 +218,9 @@ func TestStrategy(t *testing.T) { scope = []string{} // make request - var mr = func(t *testing.T, provider, request string, fv url.Values) (*http.Response, []byte) { + var mr = func(t *testing.T, provider string, request uuid.UUID, fv url.Values) (*http.Response, []byte) { fv.Set("provider", provider) - res, err := newClient(t).PostForm(ts.URL+oidc.BasePath+"/auth/"+request, fv) + res, err := newClient(t).PostForm(ts.URL+oidc.BasePath+"/auth/"+request.String(), fv) require.NoError(t, err) body, err := ioutil.ReadAll(res.Body) @@ -240,6 +240,14 @@ func TestStrategy(t *testing.T) { assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), reason, "%s", body) } + // assert system error (redirect to error endpoint) + var asem = func(t *testing.T, res *http.Response, body []byte, code int, reason string) { + require.Contains(t, res.Request.URL.String(), errTS.URL, "%s", body) + + assert.Equal(t, int64(code), gjson.GetBytes(body, "0.code").Int(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "0.message").String(), reason, "%s", body) + } + // assert ui error (redirect to login/registration ui endpoint) var aue = func(t *testing.T, res *http.Response, body []byte, reason string) { require.Contains(t, res.Request.URL.String(), uiTS.URL, "%s", body) @@ -255,15 +263,14 @@ func TestStrategy(t *testing.T) { // new login request var nlr = func(t *testing.T, redirectTo string, exp time.Duration) *login.Request { r := &login.Request{ - ID: uuid.New().String(), - RequestURL: redirectTo, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(exp), - RequestHeaders: map[string][]string{}, + ID: x.NewUUID(), + RequestURL: redirectTo, + IssuedAt: time.Now(), + ExpiresAt: time.Now().Add(exp), Methods: map[identity.CredentialsType]*login.RequestMethod{ identity.CredentialsTypeOIDC: { Method: identity.CredentialsTypeOIDC, - Config: oidc.NewRequestMethodConfig(form.NewHTMLForm("")), + Config: &login.RequestMethodConfig{RequestMethodConfigurator: oidc.NewRequestMethodConfig(form.NewHTMLForm(""))}, }, }, } @@ -274,15 +281,14 @@ func TestStrategy(t *testing.T) { // new registration request var nrr = func(t *testing.T, redirectTo string, exp time.Duration) *registration.Request { r := ®istration.Request{ - ID: uuid.New().String(), - RequestURL: redirectTo, - IssuedAt: time.Now(), - ExpiresAt: time.Now().Add(exp), - RequestHeaders: map[string][]string{}, + ID: x.NewUUID(), + RequestURL: redirectTo, + IssuedAt: time.Now(), + ExpiresAt: time.Now().Add(exp), Methods: map[identity.CredentialsType]*registration.RequestMethod{ identity.CredentialsTypeOIDC: { Method: identity.CredentialsTypeOIDC, - Config: oidc.NewRequestMethodConfig(form.NewHTMLForm("")), + Config: ®istration.RequestMethodConfig{RequestMethodConfigurator: oidc.NewRequestMethodConfig(form.NewHTMLForm(""))}, }, }, } @@ -291,18 +297,21 @@ func TestStrategy(t *testing.T) { } t.Run("case=should fail because provider does not exist", func(t *testing.T) { - res, body := mr(t, "provider-does-not-exist", "request-does-not-exist", url.Values{}) + requestDoesNotExist := x.NewUUID() + res, body := mr(t, "provider-does-not-exist", requestDoesNotExist, url.Values{}) ase(t, res, body, http.StatusNotFound, "is unknown or has not been configured") }) t.Run("case=should fail because the issuer is mismatching", func(t *testing.T) { - res, body := mr(t, "invalid-issuer", "request-does-not-exist", url.Values{}) + requestDoesNotExist := x.NewUUID() + res, body := mr(t, "invalid-issuer", requestDoesNotExist, url.Values{}) ase(t, res, body, http.StatusInternalServerError, "issuer did not match the issuer returned by provider") }) t.Run("case=should fail because request does not exist", func(t *testing.T) { - res, body := mr(t, "valid", "request-does-not-exist", url.Values{}) - ase(t, res, body, http.StatusNotFound, "Unable to find request") + requestDoesNotExist := x.NewUUID() + res, body := mr(t, "valid", requestDoesNotExist, url.Values{}) + asem(t, res, body, http.StatusNotFound, "Unable to locate the resource") }) t.Run("case=should fail because the login request is expired", func(t *testing.T) { @@ -418,10 +427,9 @@ func TestStrategy(t *testing.T) { i.SetCredentials(identity.CredentialsTypePassword, identity.Credentials{ Identifiers: []string{subject}, }) - i.Traits = json.RawMessage(`{"subject":"` + subject + `"}`) + i.Traits = identity.Traits(`{"subject":"` + subject + `"}`) - _, err := reg.IdentityPool().Create(context.Background(), i) - require.NoError(t, err) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), i)) }) t.Run("case=should fail registration", func(t *testing.T) { diff --git a/selfservice/strategy/oidc/types.go b/selfservice/strategy/oidc/types.go index 35470cc028a..17b586b6d1c 100644 --- a/selfservice/strategy/oidc/types.go +++ b/selfservice/strategy/oidc/types.go @@ -1,6 +1,8 @@ package oidc import ( + "github.com/gofrs/uuid" + "github.com/ory/kratos/selfservice/form" ) @@ -28,5 +30,5 @@ func NewRequestMethodConfig(f *form.HTMLForm) *RequestMethod { } type request interface { - GetID() string + GetID() uuid.UUID } diff --git a/selfservice/strategy/password/login.go b/selfservice/strategy/password/login.go index cd20b2c32fd..4404e433679 100644 --- a/selfservice/strategy/password/login.go +++ b/selfservice/strategy/password/login.go @@ -45,9 +45,9 @@ func (s *Strategy) handleLoginError(w http.ResponseWriter, r *http.Request, rr * } func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rid := r.URL.Query().Get("request") - if len(rid) == 0 { - s.handleLoginError(w, r, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The request Code is missing."))) + rid := x.ParseUUID(r.URL.Query().Get("request")) + if x.IsZeroUUID(rid) { + s.handleLoginError(w, r, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The request query parameter is missing or invalid."))) return } @@ -77,7 +77,7 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou } if err := ar.Valid(); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } @@ -90,7 +90,7 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou var o CredentialsConfig d := json.NewDecoder(bytes.NewBuffer(c.Config)) if err := d.Decode(&o); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error())) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, herodot.ErrInternalServerError.WithReason("The password credentials could not be decoded properly").WithDebug(err.Error())) return } @@ -101,7 +101,7 @@ func (s *Strategy) handleLogin(w http.ResponseWriter, r *http.Request, _ httprou if err := s.d.LoginHookExecutor().PostLoginHook(w, r, s.d.PostLoginHooks(identity.CredentialsTypePassword), ar, i); err != nil { - s.d.ErrorManager().ForwardError(r.Context(), w, r, err) + s.d.SelfServiceErrorManager().ForwardError(r.Context(), w, r, err) return } } @@ -113,7 +113,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, sr *login.Request) error action := urlx.CopyWithQuery( urlx.AppendPaths(s.c.SelfPublicURL(), LoginPath), - url.Values{"request": {sr.ID}}, + url.Values{"request": {sr.ID.String()}}, ) f := &form.HTMLForm{ @@ -135,7 +135,7 @@ func (s *Strategy) PopulateLoginMethod(r *http.Request, sr *login.Request) error sr.Methods[identity.CredentialsTypePassword] = &login.RequestMethod{ Method: identity.CredentialsTypePassword, - Config: &RequestMethod{HTMLForm: f}, + Config: &login.RequestMethodConfig{RequestMethodConfigurator: &RequestMethod{HTMLForm: f}}, } return nil } diff --git a/selfservice/strategy/password/login_test.go b/selfservice/strategy/password/login_test.go index dad30aa00ef..ea188e5397f 100644 --- a/selfservice/strategy/password/login_test.go +++ b/selfservice/strategy/password/login_test.go @@ -13,11 +13,12 @@ import ( "testing" "time" - "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" + "github.com/ory/x/pointerx" + "github.com/ory/viper" "github.com/ory/kratos/driver/configuration" @@ -31,18 +32,18 @@ import ( "github.com/ory/kratos/x" ) -func nlr(id string, exp time.Duration) *login.Request { +func nlr(exp time.Duration) *login.Request { + id := x.NewUUID() return &login.Request{ - ID: "request-" + id, - IssuedAt: time.Now().UTC(), - ExpiresAt: time.Now().UTC().Add(exp), - RequestURL: "remove-this-if-test-fails", - RequestHeaders: http.Header{}, + ID: id, + IssuedAt: time.Now().UTC(), + ExpiresAt: time.Now().UTC().Add(exp), + RequestURL: "remove-this-if-test-fails", Methods: map[identity.CredentialsType]*login.RequestMethod{ identity.CredentialsTypePassword: { Method: identity.CredentialsTypePassword, - Config: &password.RequestMethod{ - HTMLForm: &form.HTMLForm{ + Config: &login.RequestMethodConfig{ + RequestMethodConfigurator: &form.HTMLForm{ Action: "/action", Fields: form.Fields{ "identifier": { @@ -65,7 +66,7 @@ func nlr(id string, exp time.Duration) *login.Request { Name: "request", Type: "hidden", Required: true, - Value: "request-" + id, + Value: id.String(), }, }, }, @@ -91,72 +92,72 @@ func TestLogin(t *testing.T) { } } - for k, tc := range []struct { - d string - prep func(t *testing.T, r loginStrategyDependencies) - ar *login.Request - rid string - payload string - assert func(t *testing.T, r *http.Response) - }{ + type testCase struct { + d string + prep func(t *testing.T, r loginStrategyDependencies) + ar *login.Request + forceRequestID *string + payload string + assert func(t *testing.T, tc testCase, r *http.Response) + } + + for k, tc := range []testCase{ { d: "should show the error ui because the request is malformed", - ar: nlr("0", 0), - rid: "0", + ar: nlr(0), payload: "14=)=!(%)$/ZP()GHIÖ", - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts", "%+v", r.Request) body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) - assert.Equal(t, "request-0", gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body) assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), `invalid URL escape`) }, }, { - d: "should show the error ui because the request id missing", - ar: nlr("0", time.Minute), - payload: url.Values{}.Encode(), - rid: "", - assert: func(t *testing.T, r *http.Response) { + d: "should show the error ui because the request id missing", + ar: nlr(time.Minute), + forceRequestID: pointerx.String(""), + payload: url.Values{}.Encode(), + assert: func(t *testing.T, tc testCase, r *http.Response) { assert.Contains(t, r.Request.URL.Path, "error-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) - assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(body, "0.code").Int(), "%s", body) - assert.Equal(t, "Not Found", gjson.GetBytes(body, "0.status").String(), "%s", body) - assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "Unable to find request", "%s", body) + assert.Equal(t, int64(http.StatusBadRequest), gjson.GetBytes(body, "0.code").Int(), "%s", body) + assert.Equal(t, "Bad Request", gjson.GetBytes(body, "0.status").String(), "%s", body) + assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "request query parameter is missing or invalid", "%s", body) }, }, { - d: "should return an error because the request does not exist", - ar: nlr("1", 0), - rid: "does-not-exist", + d: "should return an error because the request does not exist", + ar: nlr(0), + forceRequestID: pointerx.String(x.NewUUID().String()), payload: url.Values{ "identifier": {"identifier"}, "password": {"password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { assert.Contains(t, r.Request.URL.Path, "error-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(body, "0.code").Int(), "%s", body) assert.Equal(t, "Not Found", gjson.GetBytes(body, "0.status").String(), "%s", body) - assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "request-does-not-exist", "%s", body) + assert.Contains(t, gjson.GetBytes(body, "0.message").String(), "Unable to locate the resource", "%s", body) }, }, { - d: "should return an error because the request is expired", - ar: nlr("2", -time.Hour), - rid: "2", + d: "should return an error because the request is expired", + ar: nlr(-time.Hour), payload: url.Values{ "identifier": {"identifier"}, "password": {"password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { assert.Contains(t, r.Request.URL.Path, "error-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) @@ -167,37 +168,35 @@ func TestLogin(t *testing.T) { }, }, { - d: "should return an error because the credentials are invalid (user does not exist)", - ar: nlr("3", time.Hour), - rid: "3", + d: "should return an error because the credentials are invalid (user does not exist)", + ar: nlr(time.Hour), payload: url.Values{ "identifier": {"identifier"}, "password": {"password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) - assert.Equal(t, "request-3", gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String()) assert.Equal(t, `The provided credentials are invalid. Check for spelling mistakes in your password or username, email address, or phone number.`, gjson.GetBytes(body, "methods.password.config.errors.0.message").String()) }, }, { - d: "should return an error because no identifier is set", - ar: nlr("4", time.Hour), - rid: "4", + d: "should return an error because no identifier is set", + ar: nlr(time.Hour), payload: url.Values{ "password": {"password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) // Let's ensure that the payload is being propagated properly. - assert.Equal(t, "request-4", gjson.GetBytes(body, "id").String()) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String()) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String()) ensureFieldsExist(t, body) assert.Equal(t, "identifier: identifier is required", gjson.GetBytes(body, "methods.password.config.fields.identifier.errors.0.message").String(), "%s", body) @@ -207,19 +206,18 @@ func TestLogin(t *testing.T) { }, }, { - d: "should return an error because no password is set", - ar: nlr("5", time.Hour), - rid: "5", + d: "should return an error because no password is set", + ar: nlr(time.Hour), payload: url.Values{ "identifier": {"identifier"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) // Let's ensure that the payload is being propagated properly. - assert.Equal(t, "request-5", gjson.GetBytes(body, "id").String()) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String()) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String()) ensureFieldsExist(t, body) assert.Equal(t, "password: password is required", gjson.GetBytes(body, "methods.password.config.fields.password.errors.0.message").String(), "%s", body) @@ -233,33 +231,31 @@ func TestLogin(t *testing.T) { }, { d: "should return an error because the credentials are invalid (password not correct)", - ar: nlr("6", time.Hour), + ar: nlr(time.Hour), prep: func(t *testing.T, r loginStrategyDependencies) { p, _ := r.PasswordHasher().Generate([]byte("password")) - _, err := r.IdentityPool().Create(context.Background(), &identity.Identity{ - ID: uuid.New().String(), - Traits: json.RawMessage(`{}`), + require.NoError(t, r.IdentityPool().CreateIdentity(context.Background(), &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{}`), Credentials: map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { - ID: identity.CredentialsTypePassword, + Type: identity.CredentialsTypePassword, Identifiers: []string{"login-identifier-6"}, Config: json.RawMessage(`{"hashed_password":"` + string(p) + `"}`), }, }, - }) - require.NoError(t, err) + })) }, - rid: "6", payload: url.Values{ "identifier": {"login-identifier-6"}, "password": {"not-password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) - assert.Equal(t, "request-6", gjson.GetBytes(body, "id").String()) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String()) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String()) ensureFieldsExist(t, body) assert.Equal(t, schema.NewInvalidCredentialsError().(schema.ResultErrors)[0].Description(), gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), "%s", body) @@ -270,28 +266,26 @@ func TestLogin(t *testing.T) { }, { d: "should pass because everything is a-ok", - ar: nlr("7", time.Hour), + ar: nlr(time.Hour), prep: func(t *testing.T, r loginStrategyDependencies) { p, _ := r.PasswordHasher().Generate([]byte("password")) - _, err := r.IdentityPool().Create(context.Background(), &identity.Identity{ - ID: uuid.New().String(), - Traits: json.RawMessage(`{"subject":"login-identifier-7"}`), + require.NoError(t, r.IdentityPool().CreateIdentity(context.Background(), &identity.Identity{ + ID: x.NewUUID(), + Traits: identity.Traits(`{"subject":"login-identifier-7"}`), Credentials: map[identity.CredentialsType]identity.Credentials{ identity.CredentialsTypePassword: { - ID: identity.CredentialsTypePassword, + Type: identity.CredentialsTypePassword, Identifiers: []string{"login-identifier-7"}, Config: json.RawMessage(`{"hashed_password":"` + string(p) + `"}`), }, }, - }) - require.NoError(t, err) + })) }, - rid: "7", payload: url.Values{ "identifier": {"login-identifier-7"}, "password": {"password"}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { assert.Contains(t, r.Request.URL.Path, "return-ts", "%s", r.Request.URL.String()) body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) @@ -300,28 +294,29 @@ func TestLogin(t *testing.T) { }, }, { - d: "should return an error because not passing validation and reset previous errors and values", - rid: "8", + d: "should return an error because not passing validation and reset previous errors and values", ar: &login.Request{ - ID: "request-8", + ID: x.NewUUID(), ExpiresAt: time.Now().Add(time.Minute), Methods: map[identity.CredentialsType]*login.RequestMethod{ identity.CredentialsTypePassword: { Method: identity.CredentialsTypePassword, - Config: &password.RequestMethod{ - HTMLForm: &form.HTMLForm{ - Action: "/action", - Errors: []form.Error{{Message: "some error"}}, - Fields: form.Fields{ - "identifier": { - Value: "baz", - Name: "identifier", - Errors: []form.Error{{Message: "err"}}, - }, - "password": { - Value: "bar", - Name: "password", - Errors: []form.Error{{Message: "err"}}, + Config: &login.RequestMethodConfig{ + RequestMethodConfigurator: &password.RequestMethod{ + HTMLForm: &form.HTMLForm{ + Action: "/action", + Errors: []form.Error{{Message: "some error"}}, + Fields: form.Fields{ + "identifier": { + Value: "baz", + Name: "identifier", + Errors: []form.Error{{Message: "err"}}, + }, + "password": { + Value: "bar", + Name: "password", + Errors: []form.Error{{Message: "err"}}, + }, }, }, }, @@ -333,12 +328,12 @@ func TestLogin(t *testing.T) { "identifier": {"registration-identifier-9"}, // "password": {uuid.New().String()}, }.Encode(), - assert: func(t *testing.T, r *http.Response) { + assert: func(t *testing.T, tc testCase, r *http.Response) { require.Contains(t, r.Request.URL.Path, "login-ts") body, err := ioutil.ReadAll(r.Body) require.NoError(t, err) - assert.Equal(t, "request-8", gjson.GetBytes(body, "id").String()) + assert.Equal(t, tc.ar.ID.String(), gjson.GetBytes(body, "id").String()) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String()) ensureFieldsExist(t, body) @@ -350,7 +345,7 @@ func TestLogin(t *testing.T) { }, } { t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) s := reg.LoginStrategies().MustStrategy(identity.CredentialsTypePassword).(*password.Strategy) s.WithTokenGenerator(func(r *http.Request) string { return "anti-rf-token" @@ -363,7 +358,7 @@ func TestLogin(t *testing.T) { defer ts.Close() errTs, uiTs, returnTs := errorx.NewErrorTestServer(t, reg), httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e, err := reg.LoginRequestPersister().GetLoginRequest(context.Background(), r.URL.Query().Get("request")) + e, err := reg.LoginRequestPersister().GetLoginRequest(context.Background(), x.ParseUUID(r.URL.Query().Get("request"))) require.NoError(t, err) reg.Writer().Write(w, r, e) })), newReturnTs(t, reg) @@ -387,12 +382,17 @@ func TestLogin(t *testing.T) { c := ts.Client() c.Jar, _ = cookiejar.New(&cookiejar.Options{}) - res, err := c.Post(ts.URL+password.LoginPath+"?request=request-"+tc.rid, "application/x-www-form-urlencoded", strings.NewReader(tc.payload)) + requestID := tc.ar.ID.String() + if tc.forceRequestID != nil { + requestID = *tc.forceRequestID + } + + res, err := c.Post(ts.URL+password.LoginPath+"?request="+requestID, "application/x-www-form-urlencoded", strings.NewReader(tc.payload)) require.NoError(t, err) defer res.Body.Close() require.EqualValues(t, http.StatusOK, res.StatusCode, "Request: %+v\n\t\tResponse: %s", res.Request, res) - tc.assert(t, res) + tc.assert(t, tc, res) }) } } diff --git a/selfservice/strategy/password/registration.go b/selfservice/strategy/password/registration.go index 54d51b3cb4b..958b1cb28d0 100644 --- a/selfservice/strategy/password/registration.go +++ b/selfservice/strategy/password/registration.go @@ -10,6 +10,8 @@ import ( "github.com/pkg/errors" "github.com/tidwall/sjson" + "github.com/ory/x/errorsx" + "github.com/ory/x/decoderx" _ "github.com/ory/x/jsonschemax/fileloader" _ "github.com/ory/x/jsonschemax/httploader" @@ -94,8 +96,8 @@ func (s *Strategy) decoderRegistration() (decoderx.HTTPDecoderOption, error) { } func (s *Strategy) handleRegistration(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - rid := r.URL.Query().Get("request") - if len(rid) == 0 { + rid := x.ParseUUID(r.URL.Query().Get("request")) + if x.IsZeroUUID(rid) { s.handleRegistrationError(w, r, nil, nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("The request Code is missing."))) return } @@ -150,9 +152,9 @@ func (s *Strategy) handleRegistration(w http.ResponseWriter, r *http.Request, _ } i := identity.NewIdentity(s.c.DefaultIdentityTraitsSchemaURL().String()) - i.Traits = p.Traits + i.Traits = identity.Traits(p.Traits) i.SetCredentials(s.ID(), identity.Credentials{ - ID: s.ID(), + Type: s.ID(), Identifiers: []string{}, Config: json.RawMessage(co), }) @@ -166,7 +168,7 @@ func (s *Strategy) handleRegistration(w http.ResponseWriter, r *http.Request, _ s.d.PostRegistrationHooks(identity.CredentialsTypePassword), ar, i, - ); errors.Cause(err) == registration.ErrHookAbortRequest { + ); errorsx.Cause(err) == registration.ErrHookAbortRequest { return } else if err != nil { s.handleRegistrationError(w, r, ar, &p, err) @@ -189,7 +191,7 @@ func (s *Strategy) validateCredentials(i *identity.Identity, pw string) error { for _, id := range c.Identifiers { if err := s.d.PasswordValidator().Validate(id, pw); err != nil { - if _, ok := errors.Cause(err).(*herodot.DefaultError); ok { + if _, ok := errorsx.Cause(err).(*herodot.DefaultError); ok { return err } return schema.NewPasswordPolicyValidation( @@ -206,7 +208,7 @@ func (s *Strategy) validateCredentials(i *identity.Identity, pw string) error { func (s *Strategy) PopulateRegistrationMethod(r *http.Request, sr *registration.Request) error { action := urlx.CopyWithQuery( urlx.AppendPaths(s.c.SelfPublicURL(), RegistrationPath), - url.Values{"request": {sr.ID}}, + url.Values{"request": {sr.ID.String()}}, ) htmlf, err := form.NewHTMLFormFromJSONSchema(action.String(), s.c.DefaultIdentityTraitsSchemaURL().String(), "traits") @@ -220,7 +222,7 @@ func (s *Strategy) PopulateRegistrationMethod(r *http.Request, sr *registration. sr.Methods[identity.CredentialsTypePassword] = ®istration.RequestMethod{ Method: identity.CredentialsTypePassword, - Config: &RequestMethod{HTMLForm: htmlf}, + Config: ®istration.RequestMethodConfig{RequestMethodConfigurator: &RequestMethod{HTMLForm: htmlf}}, } return nil diff --git a/selfservice/strategy/password/registration_test.go b/selfservice/strategy/password/registration_test.go index 5c98ffc3c4d..36158341091 100644 --- a/selfservice/strategy/password/registration_test.go +++ b/selfservice/strategy/password/registration_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" @@ -40,7 +40,7 @@ func fieldNameSet(t *testing.T, body []byte, fields ...string) { func TestRegistration(t *testing.T) { t.Run("case=registration", func(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) s := reg.RegistrationStrategies().MustStrategy(identity.CredentialsTypePassword).(*password.Strategy) s.WithTokenGenerator(func(r *http.Request) string { return "nosurf" @@ -53,7 +53,7 @@ func TestRegistration(t *testing.T) { defer ts.Close() errTs, uiTs, returnTs := newErrTs(t, reg), httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e, err := reg.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), r.URL.Query().Get("request")) + e, err := reg.RegistrationRequestPersister().GetRegistrationRequest(r.Context(), x.ParseUUID(r.URL.Query().Get("request"))) require.NoError(t, err) reg.Writer().Write(w, r, e) })), newReturnTs(t, reg) @@ -69,17 +69,19 @@ func TestRegistration(t *testing.T) { var newRegistrationRequest = func(t *testing.T, exp time.Duration) *registration.Request { rr := ®istration.Request{ - ID: "request-" + uuid.New().String(), IssuedAt: time.Now().UTC(), ExpiresAt: time.Now().UTC().Add(exp), RequestURL: ts.URL, - RequestHeaders: http.Header{}, + ID: x.NewUUID(), + IssuedAt: time.Now().UTC(), ExpiresAt: time.Now().UTC().Add(exp), RequestURL: ts.URL, Methods: map[identity.CredentialsType]*registration.RequestMethod{ identity.CredentialsTypePassword: { Method: identity.CredentialsTypePassword, - Config: &password.RequestMethod{ - HTMLForm: &form.HTMLForm{ - Action: "/action", - Fields: form.Fields{ - "password": {Name: "password", Type: "password", Required: true}, - "csrf_token": {Name: "csrf_token", Type: "hidden", Required: true, Value: "csrf-token"}, + Config: ®istration.RequestMethodConfig{ + RequestMethodConfigurator: password.RequestMethod{ + HTMLForm: &form.HTMLForm{ + Action: "/action", + Fields: form.Fields{ + "password": {Name: "password", Type: "password", Required: true}, + "csrf_token": {Name: "csrf_token", Type: "hidden", Required: true, Value: "csrf-token"}, + }, }, }, }, @@ -90,10 +92,10 @@ func TestRegistration(t *testing.T) { return rr } - var makeRequest = func(t *testing.T, rid, body string, expectedStatusCode int) ([]byte, *http.Response) { + var makeRequest = func(t *testing.T, rid uuid.UUID, body string, expectedStatusCode int) ([]byte, *http.Response) { jar, _ := cookiejar.New(&cookiejar.Options{}) client := http.Client{Jar: jar} - res, err := client.Post(ts.URL+password.RegistrationPath+"?request="+rid, "application/x-www-form-urlencoded", strings.NewReader(body)) + res, err := client.Post(ts.URL+password.RegistrationPath+"?request="+rid.String(), "application/x-www-form-urlencoded", strings.NewReader(body)) require.NoError(t, err) result, err := ioutil.ReadAll(res.Body) require.NoError(t, res.Body.Close()) @@ -106,24 +108,25 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, "14=)=!(%)$/ZP()GHIÖ", http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") - assert.Equal(t, rr.ID, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), "invalid URL escape", "%s", body) }) t.Run("case=should show the error ui because the request id is missing", func(t *testing.T) { _ = newRegistrationRequest(t, time.Minute) - body, res := makeRequest(t, "does-not-exist", "", http.StatusOK) + uuidDesNotExistInStore := x.NewUUID() + body, res := makeRequest(t, uuidDesNotExistInStore, "", http.StatusOK) assert.Contains(t, res.Request.URL.Path, "error-ts") assert.Equal(t, int64(http.StatusNotFound), gjson.GetBytes(body, "0.code").Int(), "%s", body) assert.Equal(t, "Not Found", gjson.GetBytes(body, "0.status").String(), "%s", body) - assert.Contains(t, gjson.GetBytes(body, "0.reason").String(), "Unable to find request", "%s", body) + assert.Contains(t, gjson.GetBytes(body, "0.message").String(), "Unable to locate the resource", "%s", body) }) t.Run("case=should return an error because the request is expired", func(t *testing.T) { rr := newRegistrationRequest(t, -time.Minute) body, res := makeRequest(t, rr.ID, "", http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") - assert.Equal(t, rr.ID, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body) assert.Contains(t, gjson.GetBytes(body, "methods.password.config.errors.0.message").String(), "expired", "%s", body) }) @@ -136,7 +139,7 @@ func TestRegistration(t *testing.T) { "traits.foobar": {"bar"}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") - assert.Equal(t, rr.ID, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body) fieldNameSet(t, body, "password", "csrf_token", "traits.username", "traits.foobar") assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.password.errors.0").String(), "data breaches and must no longer be used.", "%s", body) @@ -146,10 +149,10 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-5"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") - assert.Equal(t, rr.ID, gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body) fieldNameSet(t, body, "password", "csrf_token", "traits.username", "traits.foobar") assert.Contains(t, gjson.GetBytes(body, "methods.password.config.fields.traits\\.foobar.errors.0").String(), "foobar is required", "%s", body) @@ -160,7 +163,7 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-6"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, "traits.foobar": {"bar"}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "error-ts") @@ -174,7 +177,7 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-7"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, "traits.foobar": {"bar"}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "error-ts") @@ -188,7 +191,7 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-8"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, "traits.foobar": {"bar"}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "return-ts") @@ -200,7 +203,7 @@ func TestRegistration(t *testing.T) { rr := newRegistrationRequest(t, time.Minute) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-8"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, "traits.foobar": {"bar"}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") @@ -211,21 +214,23 @@ func TestRegistration(t *testing.T) { viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/registration.schema.json") rr := ®istration.Request{ - ID: "request-9", + ID: x.NewUUID(), ExpiresAt: time.Now().Add(time.Minute), Methods: map[identity.CredentialsType]*registration.RequestMethod{ identity.CredentialsTypePassword: { Method: identity.CredentialsTypePassword, - Config: &password.RequestMethod{ - HTMLForm: &form.HTMLForm{ - Action: "/action", - Errors: []form.Error{{Message: "some error"}}, - Fields: form.Fields{ - "traits.foo": { - Name: "traits.foo", Value: "bar", Type: "text", - Errors: []form.Error{{Message: "bar"}}, + Config: ®istration.RequestMethodConfig{ + RequestMethodConfigurator: &password.RequestMethod{ + HTMLForm: &form.HTMLForm{ + Action: "/action", + Errors: []form.Error{{Message: "some error"}}, + Fields: form.Fields{ + "traits.foo": { + Name: "traits.foo", Value: "bar", Type: "text", + Errors: []form.Error{{Message: "bar"}}, + }, + "password": {Name: "password"}, }, - "password": {Name: "password"}, }, }, }, @@ -236,10 +241,10 @@ func TestRegistration(t *testing.T) { require.NoError(t, reg.RegistrationRequestPersister().CreateRegistrationRequest(context.Background(), rr)) body, res := makeRequest(t, rr.ID, url.Values{ "traits.username": {"registration-identifier-9"}, - "password": {uuid.New().String()}, + "password": {x.NewUUID().String()}, }.Encode(), http.StatusOK) assert.Contains(t, res.Request.URL.Path, "signup-ts") - assert.Equal(t, "request-9", gjson.GetBytes(body, "id").String(), "%s", body) + assert.Equal(t, rr.ID.String(), gjson.GetBytes(body, "id").String(), "%s", body) assert.Equal(t, "/action", gjson.GetBytes(body, "methods.password.config.action").String(), "%s", body) fieldNameSet(t, body, "password", "csrf_token", "traits.username") @@ -263,7 +268,7 @@ func TestRegistration(t *testing.T) { }) t.Run("method=PopulateSignUpMethod", func(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) s := reg.RegistrationStrategies().MustStrategy(identity.CredentialsTypePassword).(*password.Strategy) s.WithTokenGenerator(func(r *http.Request) string { return "nosurf" @@ -277,35 +282,37 @@ func TestRegistration(t *testing.T) { expected := ®istration.RequestMethod{ Method: identity.CredentialsTypePassword, - Config: &password.RequestMethod{ - HTMLForm: &form.HTMLForm{ - Action: "https://foo" + password.RegistrationPath + "?request=" + sr.ID, - Method: "POST", - Fields: form.Fields{ - "password": { - Name: "password", - Type: "password", - Required: true, - }, - "csrf_token": { - Name: "csrf_token", - Type: "hidden", - Required: true, - Value: "nosurf", - }, - "traits.foobar": { - Name: "traits.foobar", - Type: "text", - }, - "traits.username": { - Name: "traits.username", - Type: "text", + Config: ®istration.RequestMethodConfig{ + RequestMethodConfigurator: &password.RequestMethod{ + HTMLForm: &form.HTMLForm{ + Action: "https://foo" + password.RegistrationPath + "?request=" + sr.ID.String(), + Method: "POST", + Fields: form.Fields{ + "password": { + Name: "password", + Type: "password", + Required: true, + }, + "csrf_token": { + Name: "csrf_token", + Type: "hidden", + Required: true, + Value: "nosurf", + }, + "traits.foobar": { + Name: "traits.foobar", + Type: "text", + }, + "traits.username": { + Name: "traits.username", + Type: "text", + }, }, }, }, }, } actual := sr.Methods[identity.CredentialsTypePassword] - assert.EqualValues(t, expected.Config.(*password.RequestMethod).HTMLForm, actual.Config.(*password.RequestMethod).HTMLForm) + assert.EqualValues(t, expected.Config.RequestMethodConfigurator.(*password.RequestMethod).HTMLForm, actual.Config.RequestMethodConfigurator.(*password.RequestMethod).HTMLForm) }) } diff --git a/selfservice/strategy/password/strategy_test.go b/selfservice/strategy/password/strategy_test.go index 13b95f2dcfb..ae4645fd5c6 100644 --- a/selfservice/strategy/password/strategy_test.go +++ b/selfservice/strategy/password/strategy_test.go @@ -16,11 +16,11 @@ import ( ) func newErrTs(t *testing.T, reg interface { - errorx.ManagementProvider + errorx.PersistenceProvider x.WriterProvider }) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - e, err := reg.ErrorManager().Read(r.Context(), r.URL.Query().Get("error")) + e, err := reg.SelfServiceErrorPersister().Read(r.Context(), x.ParseUUID(r.URL.Query().Get("error"))) require.NoError(t, err) reg.Writer().Write(w, r, e) })) diff --git a/session/handler.go b/session/handler.go index e6066f06a60..798746eb6a2 100644 --- a/session/handler.go +++ b/session/handler.go @@ -6,23 +6,32 @@ import ( "github.com/julienschmidt/httprouter" "github.com/pkg/errors" + "github.com/ory/x/errorsx" + "github.com/ory/herodot" "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/x" ) -type HandlerProvider interface { - SessionHandler() *Handler -} +type ( + handlerDependencies interface { + ManagementProvider + x.WriterProvider + } + HandlerProvider interface { + SessionHandler() *Handler + } + Handler struct { + r handlerDependencies + } +) func NewHandler( - r Registry, - h herodot.Writer, + r handlerDependencies, ) *Handler { return &Handler{ r: r, - h: h, } } @@ -30,11 +39,6 @@ const ( CheckPath = "/sessions/me" ) -type Handler struct { - r Registry - h herodot.Writer -} - func (h *Handler) RegisterPublicRoutes(public *x.RouterPublic) { public.GET(CheckPath, h.fromCookie) } @@ -46,14 +50,14 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { func (h *Handler) fromCookie(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { s, err := h.r.SessionManager().FetchFromRequest(r.Context(), w, r) if err != nil { - h.h.WriteError(w, r, err) + h.r.Writer().WriteError(w, r, err) return } // s.Devices = nil s.Identity = s.Identity.CopyWithoutCredentials() - h.h.Write(w, r, s) + h.r.Writer().Write(w, r, s) } func (h *Handler) fromPath(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { @@ -68,7 +72,7 @@ func (h *Handler) IsAuthenticated(wrap httprouter.Handle, onUnauthenticated http return } - h.h.WriteError(w, r, errors.WithStack(herodot.ErrForbidden.WithReason("This endpoint can only be accessed with a valid session. Please log in and try again.").WithDebugf("%+v", err))) + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden.WithReason("This endpoint can only be accessed with a valid session. Please log in and try again.").WithDebugf("%+v", err))) return } @@ -79,11 +83,11 @@ func (h *Handler) IsAuthenticated(wrap httprouter.Handle, onUnauthenticated http func (h *Handler) IsNotAuthenticated(wrap httprouter.Handle, onAuthenticated httprouter.Handle) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { if _, err := h.r.SessionManager().FetchFromRequest(r.Context(), w, r); err != nil { - if errors.Cause(err).Error() == ErrNoActiveSessionFound.Error() { + if errorsx.Cause(err).Error() == ErrNoActiveSessionFound.Error() { wrap(w, r, ps) return } - h.h.WriteError(w, r, err) + h.r.Writer().WriteError(w, r, err) return } @@ -92,7 +96,7 @@ func (h *Handler) IsNotAuthenticated(wrap httprouter.Handle, onAuthenticated htt return } - h.h.WriteError(w, r, errors.WithStack(herodot.ErrForbidden.WithReason("This endpoint can only be accessed without a login session. Please log out and try again."))) + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrForbidden.WithReason("This endpoint can only be accessed without a login session. Please log out and try again."))) } } diff --git a/session/handler_mock.go b/session/handler_mock.go index 0f06f3ab32b..9581408a792 100644 --- a/session/handler_mock.go +++ b/session/handler_mock.go @@ -19,21 +19,28 @@ import ( "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" ) -func MockSetSession(t *testing.T, reg Registry) httprouter.Handle { +type mockDeps interface { + identity.PoolProvider + ManagementProvider + PersistenceProvider +} + +func MockSetSession(t *testing.T, reg mockDeps) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - i, err := reg.IdentityPool().Create(context.Background(), identity.NewIdentity("")) - require.NoError(t, err) + i := identity.NewIdentity("") + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), i)) - _, err = reg.SessionManager().CreateToRequest(context.Background(), i, w, r) + _, err := reg.SessionManager().CreateToRequest(context.Background(), i, w, r) require.NoError(t, err) w.WriteHeader(http.StatusOK) } } -func MockGetSession(t *testing.T, reg Registry) httprouter.Handle { +func MockGetSession(t *testing.T, reg mockDeps) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { _, err := reg.SessionManager().FetchFromRequest(r.Context(), w, r) if r.URL.Query().Get("has") == "yes" { @@ -45,7 +52,7 @@ func MockGetSession(t *testing.T, reg Registry) httprouter.Handle { } } -func MockMakeAuthenticatedRequest(t *testing.T, reg Registry, router *httprouter.Router, req *http.Request) ([]byte, *http.Response) { +func MockMakeAuthenticatedRequest(t *testing.T, reg mockDeps, router *httprouter.Router, req *http.Request) ([]byte, *http.Response) { set := "/" + uuid.New().String() + "/set" router.GET(set, MockSetSession(t, reg)) @@ -85,7 +92,7 @@ func MockHydrateCookieClient(t *testing.T, c *http.Client, u string) { require.True(t, found) } -func MockSessionCreateHandlerWithIdentity(t *testing.T, reg Registry, i *identity.Identity) (httprouter.Handle, *Session) { +func MockSessionCreateHandlerWithIdentity(t *testing.T, reg mockDeps, i *identity.Identity) (httprouter.Handle, *Session) { var sess Session require.NoError(t, faker.FakeData(&sess)) @@ -93,27 +100,25 @@ func MockSessionCreateHandlerWithIdentity(t *testing.T, reg Registry, i *identit viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/fake-session.schema.json") } - _, err := reg.IdentityPool().Create(context.Background(), i) - require.NoError(t, err) + require.NoError(t, reg.IdentityPool().CreateIdentity(context.Background(), i)) - inserted, err := reg.IdentityPool().GetClassified(context.Background(), i.ID) + inserted, err := reg.IdentityPool().GetIdentityConfidential(context.Background(), i.ID) require.NoError(t, err) sess.Identity = inserted - require.NoError(t, reg.SessionManager().Create(context.Background(), &sess)) - require.EqualValues(t, inserted.Credentials, i.Credentials) + require.NoError(t, reg.SessionPersister().CreateSession(context.Background(), &sess)) + require.Len(t, inserted.Credentials, len(i.Credentials)) return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - require.NoError(t, reg.SessionManager().Create(context.Background(), &sess)) require.NoError(t, reg.SessionManager().SaveToRequest(context.Background(), &sess, w, r)) }, &sess } -func MockSessionCreateHandler(t *testing.T, reg Registry) (httprouter.Handle, *Session) { +func MockSessionCreateHandler(t *testing.T, reg mockDeps) (httprouter.Handle, *Session) { return MockSessionCreateHandlerWithIdentity(t, reg, &identity.Identity{ - ID: uuid.New().String(), + ID: x.NewUUID(), TraitsSchemaURL: "file://./stub/fake-session.schema.json", - Traits: json.RawMessage(`{"baz":"bar","foo":true,"bar":2.5}`), + Traits: identity.Traits(json.RawMessage(`{"baz":"bar","foo":true,"bar":2.5}`)), }) } diff --git a/session/handler_test.go b/session/handler_test.go index 827c372cf5a..5b8cb337aae 100644 --- a/session/handler_test.go +++ b/session/handler_test.go @@ -15,14 +15,16 @@ import ( "github.com/ory/viper" - "github.com/ory/herodot" - "github.com/ory/kratos/driver/configuration" "github.com/ory/kratos/internal" . "github.com/ory/kratos/session" "github.com/ory/kratos/x" ) +func init() { + internal.RegisterFakes() +} + func send(code int) httprouter.Handle { return func(w http.ResponseWriter, _ *http.Request, _ httprouter.Params) { w.WriteHeader(code) @@ -31,13 +33,13 @@ func send(code int) httprouter.Handle { func TestHandler(t *testing.T) { t.Run("public", func(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) r := x.NewRouterPublic() h, _ := MockSessionCreateHandler(t, reg) r.GET("/set", h) - NewHandler(reg, herodot.NewJSONWriter(nil)).RegisterPublicRoutes(r) + NewHandler(reg).RegisterPublicRoutes(r) ts := httptest.NewServer(r) defer ts.Close() @@ -61,7 +63,7 @@ func TestHandler(t *testing.T) { } func TestIsNotAuthenticatedSecurecookie(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) r := x.NewRouterPublic() r.GET("/public/with-callback", reg.SessionHandler().IsNotAuthenticated(send(http.StatusOK), send(http.StatusBadRequest))) @@ -88,7 +90,7 @@ func TestIsNotAuthenticatedSecurecookie(t *testing.T) { } func TestIsNotAuthenticated(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) r := x.NewRouterPublic() h, _ := MockSessionCreateHandler(t, reg) @@ -139,7 +141,7 @@ func TestIsNotAuthenticated(t *testing.T) { } func TestIsAuthenticated(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) + _, reg := internal.NewRegistryDefault(t) r := x.NewRouterPublic() h, _ := MockSessionCreateHandler(t, reg) diff --git a/session/manager.go b/session/manager.go index 16f0711eba6..aa6a8815fcb 100644 --- a/session/manager.go +++ b/session/manager.go @@ -4,12 +4,9 @@ import ( "context" "net/http" - "github.com/gorilla/securecookie" - "github.com/pkg/errors" + "github.com/ory/herodot" "github.com/ory/kratos/identity" - - "github.com/ory/herodot" ) // DefaultSessionCookieName returns the default cookie name for the kratos session. @@ -22,15 +19,6 @@ var ( // Manager handles identity sessions. type Manager interface { - // Get retrieves a session from the store. - Get(ctx context.Context, sid string) (*Session, error) - - // Create adds a session to the store. - Create(ctx context.Context, s *Session) error - - // Delete removes a session from the store - Delete(ctx context.Context, sid string) error - CreateToRequest(context.Context, *identity.Identity, http.ResponseWriter, *http.Request) (*Session, error) // SaveToRequest creates an HTTP session using cookies. @@ -46,89 +34,3 @@ type Manager interface { type ManagementProvider interface { SessionManager() Manager } - -type ManagerHTTP struct { - m Manager - c Configuration - cookieName string - r Registry -} - -func NewManagerHTTP( - c Configuration, - r Registry, - m Manager, -) *ManagerHTTP { - return &ManagerHTTP{ - c: c, - r: r, - cookieName: DefaultSessionCookieName, - m: m, - } -} - -func (s *ManagerHTTP) WithManager(m Manager) { - s.m = m -} - -func (s *ManagerHTTP) CreateToRequest(ctx context.Context, i *identity.Identity, w http.ResponseWriter, r *http.Request) (*Session, error) { - p := NewSession(i, r, s.c) - if err := s.m.Create(ctx, p); err != nil { - return nil, err - } - - if err := s.SaveToRequest(ctx, p, w, r); err != nil { - return nil, err - } - - return p, nil -} - -func (s *ManagerHTTP) SaveToRequest(ctx context.Context, session *Session, w http.ResponseWriter, r *http.Request) error { - cookie, _ := s.r.CookieManager().Get(r, s.cookieName) - cookie.Values["sid"] = session.SID - if err := cookie.Save(r, w); err != nil { - return errors.WithStack(err) - } - return nil -} - -func (s *ManagerHTTP) FetchFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (*Session, error) { - cookie, err := s.r.CookieManager().Get(r, s.cookieName) - if err != nil { - if _, ok := err.(securecookie.Error); ok { - // If securecookie returns an error, the HMAC is probably invalid. In that case, we really want - // to remove the cookie from the browser as it is invalid anyways. - if err := s.PurgeFromRequest(ctx, w, r); err != nil { - return nil, err - } - } - - return nil, errors.WithStack(ErrNoActiveSessionFound.WithDebug(err.Error())) - } - - sid, ok := cookie.Values["sid"].(string) - if !ok { - return nil, errors.WithStack(ErrNoActiveSessionFound) - } - - se, err := s.m.Get(ctx, sid) - if err != nil && err.Error() == herodot.ErrNotFound.Error() { - return nil, errors.WithStack(ErrNoActiveSessionFound) - } else if err != nil { - return nil, err - } - - se.Identity = se.Identity.CopyWithoutCredentials() - - return se, nil -} - -func (s *ManagerHTTP) PurgeFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { - cookie, _ := s.r.CookieManager().Get(r, s.cookieName) - cookie.Options.MaxAge = -1 - if err := cookie.Save(r, w); err != nil { - return errors.WithStack(err) - } - return nil -} diff --git a/session/manager_http.go b/session/manager_http.go new file mode 100644 index 00000000000..85852c6c57e --- /dev/null +++ b/session/manager_http.go @@ -0,0 +1,105 @@ +package session + +import ( + "context" + "net/http" + "time" + + "github.com/gorilla/securecookie" + "github.com/pkg/errors" + + "github.com/ory/herodot" + + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" +) + +type ( + managerHTTPDependencies interface { + PersistenceProvider + x.CookieProvider + identity.PoolProvider + } + managerHTTPConfiguration interface { + SessionLifespan() time.Duration + SessionSecrets() [][]byte + } + ManagerHTTP struct { + c managerHTTPConfiguration + cookieName string + r managerHTTPDependencies + } +) + +func NewManagerHTTP( + c managerHTTPConfiguration, + r managerHTTPDependencies, +) *ManagerHTTP { + return &ManagerHTTP{ + c: c, + r: r, + cookieName: DefaultSessionCookieName, + } +} + +func (s *ManagerHTTP) CreateToRequest(ctx context.Context, i *identity.Identity, w http.ResponseWriter, r *http.Request) (*Session, error) { + p := NewSession(i, r, s.c) + if err := s.r.SessionPersister().CreateSession(ctx, p); err != nil { + return nil, err + } + + if err := s.SaveToRequest(ctx, p, w, r); err != nil { + return nil, err + } + + return p, nil +} + +func (s *ManagerHTTP) SaveToRequest(ctx context.Context, session *Session, w http.ResponseWriter, r *http.Request) error { + cookie, _ := s.r.CookieManager().Get(r, s.cookieName) + cookie.Values["sid"] = session.ID.String() + if err := cookie.Save(r, w); err != nil { + return errors.WithStack(err) + } + return nil +} + +func (s *ManagerHTTP) FetchFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (*Session, error) { + cookie, err := s.r.CookieManager().Get(r, s.cookieName) + if err != nil { + if _, ok := err.(securecookie.Error); ok { + // If securecookie returns an error, the HMAC is probably invalid. In that case, we really want + // to remove the cookie from the browser as it is invalid anyways. + if err := s.PurgeFromRequest(ctx, w, r); err != nil { + return nil, err + } + } + + return nil, errors.WithStack(ErrNoActiveSessionFound.WithDebug(err.Error())) + } + + sid, ok := cookie.Values["sid"].(string) + if !ok { + return nil, errors.WithStack(ErrNoActiveSessionFound) + } + + se, err := s.r.SessionPersister().GetSession(ctx, x.ParseUUID(sid)) + if err != nil && err.Error() == herodot.ErrNotFound.Error() { + return nil, errors.WithStack(ErrNoActiveSessionFound) + } else if err != nil { + return nil, err + } + + se.Identity = se.Identity.CopyWithoutCredentials() + + return se, nil +} + +func (s *ManagerHTTP) PurgeFromRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + cookie, _ := s.r.CookieManager().Get(r, s.cookieName) + cookie.Options.MaxAge = -1 + if err := cookie.Save(r, w); err != nil { + return errors.WithStack(err) + } + return nil +} diff --git a/session/manager_memory.go b/session/manager_memory.go deleted file mode 100644 index d2a8632a9af..00000000000 --- a/session/manager_memory.go +++ /dev/null @@ -1,55 +0,0 @@ -package session - -import ( - "context" - "sync" - - "github.com/pkg/errors" - - "github.com/ory/herodot" -) - -var _ Manager = new(ManagerMemory) - -type ManagerMemory struct { - *ManagerHTTP - sync.RWMutex - sessions map[string]Session -} - -func NewManagerMemory(c Configuration, r Registry) *ManagerMemory { - m := &ManagerMemory{sessions: make(map[string]Session)} - m.ManagerHTTP = NewManagerHTTP(c, r, m) - return m -} - -func (s *ManagerMemory) Get(ctx context.Context, sid string) (*Session, error) { - s.RLock() - defer s.RUnlock() - if r, ok := s.sessions[sid]; ok { - i, err := s.r.IdentityPool().Get(ctx, r.Identity.ID) - if err != nil { - return nil, errors.WithStack(err) - } - r.Identity = i - return &r, nil - } - - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("Unable to find session with id: %s", sid)) -} - -func (s *ManagerMemory) Create(ctx context.Context, session *Session) error { - s.Lock() - defer s.Unlock() - insert := *session - insert.Identity = insert.Identity.CopyWithoutCredentials() - s.sessions[session.SID] = insert - return nil -} - -func (s *ManagerMemory) Delete(ctx context.Context, sid string) error { - s.Lock() - defer s.Unlock() - delete(s.sessions, sid) - return nil -} diff --git a/session/manager_sql.go b/session/manager_sql.go deleted file mode 100644 index 7190f7c90fd..00000000000 --- a/session/manager_sql.go +++ /dev/null @@ -1,96 +0,0 @@ -package session - -import ( - "context" - "fmt" - "time" - - "github.com/jmoiron/sqlx" - "github.com/pkg/errors" - - "github.com/ory/x/sqlcon" - "github.com/ory/x/sqlxx" - - "github.com/ory/herodot" -) - -var _ Manager = new(ManagerSQL) - -type ManagerSQL struct { - *ManagerHTTP - db *sqlx.DB -} - -type sessionSQL struct { - SID string `db:"sid"` - ExpiresAt time.Time `db:"expires_at"` - AuthenticatedAt time.Time `db:"authenticated_at"` - IssuedAt time.Time `db:"issued_at"` - IdentityPK uint64 `db:"identity_pk"` - IdentityID string `db:"identity_id"` -} - -const sessionSQLTableName = "session" - -func NewManagerSQL(c Configuration, r Registry, db *sqlx.DB) *ManagerSQL { - m := &ManagerSQL{db: db} - m.ManagerHTTP = NewManagerHTTP(c, r, m) - return m -} - -func (s *ManagerSQL) Get(ctx context.Context, sid string) (*Session, error) { - var interim sessionSQL - columns, _ := sqlxx.NamedInsertArguments(interim, "identity_id") - query := fmt.Sprintf("SELECT %s, i.id as identity_id FROM %s JOIN identity as i ON (i.pk = identity_pk) WHERE sid=?", columns, sessionSQLTableName) - if err := sqlcon.HandleError(s.db.GetContext(ctx, &interim, s.db.Rebind(query), sid)); err != nil { - if errors.Cause(err) == sqlcon.ErrNoRows { - return nil, errors.WithStack(herodot.ErrNotFound.WithReasonf("%s", err)) - } - return nil, err - } - - i, err := s.r.IdentityPool().Get(ctx, interim.IdentityID) - if err != nil { - return nil, err - } - - return &Session{ - SID: interim.SID, - ExpiresAt: interim.ExpiresAt, - AuthenticatedAt: interim.AuthenticatedAt, - IssuedAt: interim.IssuedAt, - Identity: i, - }, nil -} - -func (s *ManagerSQL) Create(ctx context.Context, session *Session) error { - var pk uint64 - if err := sqlcon.HandleError(s.db.GetContext(ctx, &pk, s.db.Rebind("SELECT pk FROM identity WHERE id=?"), session.Identity.ID)); err != nil { - if errors.Cause(err) == sqlcon.ErrNoRows { - return errors.WithStack(herodot.ErrNotFound.WithReasonf("%s", err)) - } - return err - } - - insert := &sessionSQL{ - SID: session.SID, - ExpiresAt: session.ExpiresAt, - AuthenticatedAt: session.AuthenticatedAt, - IssuedAt: session.IssuedAt, - IdentityPK: pk, - } - - columns, arguments := sqlxx.NamedInsertArguments(insert, "identity_id") - query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)", sessionSQLTableName, columns, arguments) - if _, err := s.db.NamedExecContext(ctx, query, insert); err != nil { - return sqlcon.HandleError(err) - } - - return nil -} - -func (s *ManagerSQL) Delete(ctx context.Context, sid string) error { - query := fmt.Sprintf("DELETE FROM %s WHERE sid = ?", sessionSQLTableName) - _, err := s.db.ExecContext(ctx, s.db.Rebind(query), sid) - return sqlcon.HandleError(err) -} diff --git a/session/manager_test.go b/session/manager_test.go deleted file mode 100644 index 5e77ccff316..00000000000 --- a/session/manager_test.go +++ /dev/null @@ -1,213 +0,0 @@ -package session_test - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "net/http" - "net/http/cookiejar" - "net/http/httptest" - "sync" - "testing" - - "github.com/google/uuid" - "github.com/julienschmidt/httprouter" - "github.com/pkg/errors" - - "github.com/ory/x/sqlcon/dockertest" - - "github.com/ory/viper" - - "github.com/ory/herodot" - - "github.com/bxcodec/faker" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/ory/kratos/driver/configuration" - "github.com/ory/kratos/identity" - "github.com/ory/kratos/internal" - . "github.com/ory/kratos/session" -) - -func init() { - internal.RegisterFakes() -} - -// nolint: staticcheck -func TestMain(m *testing.M) { - flag.Parse() - runner := dockertest.Register() - runner.Exit(m.Run()) -} - -func fakeIdentity(t *testing.T, reg Registry) *identity.Identity { - i := &identity.Identity{ - ID: uuid.New().String(), - TraitsSchemaURL: "file://./stub/identity.schema.json", - Traits: json.RawMessage(`{}`), - } - - viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") - - _, err := reg.IdentityPool().Create(context.Background(), i) - require.NoError(t, err) - return i -} - -func TestSessionManager(t *testing.T) { - _, reg := internal.NewMemoryRegistry(t) - registries := map[string]Registry{ - "memory": reg, - } - - if !testing.Short() { - var l sync.Mutex - dockertest.Parallel([]func(){ - func() { - db, err := dockertest.ConnectToTestPostgreSQL() - require.NoError(t, err) - - _, reg := internal.NewRegistrySQL(t, db) - - l.Lock() - registries["postgres"] = reg - l.Unlock() - }, - }) - } - - for name, sm := range registries { - t.Run(fmt.Sprintf("manager=%s", name), func(t *testing.T) { - _, err := sm.SessionManager().Get(context.Background(), "does-not-exist") - require.Error(t, err) - - var gave Session - require.NoError(t, faker.FakeData(&gave)) - gave.Identity = fakeIdentity(t, registries[name]) - - require.NoError(t, sm.SessionManager().Create(context.Background(), &gave)) - - got, err := sm.SessionManager().Get(context.Background(), gave.SID) - require.NoError(t, err) - assert.Equal(t, gave.Identity.ID, got.Identity.ID) - assert.Equal(t, gave.SID, got.SID) - assert.EqualValues(t, gave.ExpiresAt.Unix(), got.ExpiresAt.Unix()) - assert.Equal(t, gave.AuthenticatedAt.Unix(), got.AuthenticatedAt.Unix()) - assert.Equal(t, gave.IssuedAt.Unix(), got.IssuedAt.Unix()) - - require.NoError(t, sm.SessionManager().Delete(context.Background(), gave.SID)) - _, err = sm.SessionManager().Get(context.Background(), gave.SID) - require.Error(t, err) - }) - } -} - -func TestSessionManagerHTTP(t *testing.T) { - conf, reg := internal.NewMemoryRegistry(t) - sm := NewManagerMemory(conf, reg) - h := herodot.NewJSONWriter(nil) - - var s Session - require.NoError(t, faker.FakeData(&s)) - s.Identity = fakeIdentity(t, reg) - - router := httprouter.New() - router.GET("/set", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - require.NoError(t, sm.Create(context.Background(), &s)) - require.NoError(t, sm.SaveToRequest(context.Background(), &s, w, r)) - }) - - router.GET("/set-direct", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - _, err := sm.CreateToRequest(context.Background(), s.Identity, w, r) - require.NoError(t, err) - }) - - router.GET("/clear", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - require.NoError(t, sm.PurgeFromRequest(context.Background(), w, r)) - }) - - router.GET("/get", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - s, err := sm.FetchFromRequest(context.Background(), w, r) - if errors.Cause(err) == ErrNoActiveSessionFound { - w.WriteHeader(http.StatusUnauthorized) - return - } else if err != nil { - w.WriteHeader(http.StatusInternalServerError) - assert.NoError(t, err) - return - } - - h.Write(w, r, s) - }) - - ts := httptest.NewServer(router) - defer ts.Close() - - t.Run("case=exists", func(t *testing.T) { - c := &http.Client{} - c.Jar, _ = cookiejar.New(nil) - - res, err := c.Get(ts.URL + "/set") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - res, err = c.Get(ts.URL + "/get") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - var got Session - require.NoError(t, json.NewDecoder(res.Body).Decode(&got)) - }) - - t.Run("case=exists-direct", func(t *testing.T) { - c := &http.Client{} - c.Jar, _ = cookiejar.New(nil) - - res, err := c.Get(ts.URL + "/set-direct") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - res, err = c.Get(ts.URL + "/get") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - var got Session - require.NoError(t, json.NewDecoder(res.Body).Decode(&got)) - }) - - t.Run("case=exists_clears", func(t *testing.T) { - c := &http.Client{} - c.Jar, _ = cookiejar.New(nil) - - res, err := c.Get(ts.URL + "/set") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - res, err = c.Get(ts.URL + "/clear") - require.NoError(t, err) - defer res.Body.Close() - require.EqualValues(t, http.StatusOK, res.StatusCode) - - res, err = c.Get(ts.URL + "/get") - require.NoError(t, err) - defer res.Body.Close() - assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) - }) - - t.Run("case=not-exist", func(t *testing.T) { - c := &http.Client{} - c.Jar, _ = cookiejar.New(nil) - - res, err := c.Get(ts.URL + "/get") - require.NoError(t, err) - defer res.Body.Close() - assert.EqualValues(t, http.StatusUnauthorized, res.StatusCode) - }) -} diff --git a/session/persistence.go b/session/persistence.go new file mode 100644 index 00000000000..16d39829c04 --- /dev/null +++ b/session/persistence.go @@ -0,0 +1,79 @@ +package session + +import ( + "context" + "testing" + + "github.com/bxcodec/faker" + "github.com/gofrs/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ory/viper" + + "github.com/ory/kratos/driver/configuration" + "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" +) + +type PersistenceProvider interface { + SessionPersister() Persister +} + +type Persister interface { + // Get retrieves a session from the store. + GetSession(ctx context.Context, sid uuid.UUID) (*Session, error) + + // Create adds a session to the store. + CreateSession(ctx context.Context, s *Session) error + + // Delete removes a session from the store + DeleteSession(ctx context.Context, sid uuid.UUID) error +} + +func TestPersister(p interface { + Persister + identity.Pool +}) func(t *testing.T) { + return func(t *testing.T) { + viper.Set(configuration.ViperKeyDefaultIdentityTraitsSchemaURL, "file://./stub/identity.schema.json") + + t.Run("case=not found", func(t *testing.T) { + _, err := p.GetSession(context.Background(), x.NewUUID()) + require.Error(t, err) + }) + + t.Run("case=create session", func(t *testing.T) { + var expected Session + require.NoError(t, faker.FakeData(&expected)) + require.NoError(t, p.CreateIdentity(context.Background(), expected.Identity)) + + now := expected.ID + t.Logf("now: %s", now) + assert.NotEqual(t, uuid.Nil, expected.ID) + require.NoError(t, p.CreateSession(context.Background(), &expected)) + assert.NotEqual(t, uuid.Nil, expected.ID) + later := expected.ID + t.Logf("later: %s", later) + + actual, err := p.GetSession(context.Background(), expected.ID) + require.NoError(t, err) + assert.Equal(t, expected.Identity.ID, actual.Identity.ID) + assert.Equal(t, expected.ID, actual.ID) + assert.EqualValues(t, expected.ExpiresAt.Unix(), actual.ExpiresAt.Unix()) + assert.Equal(t, expected.AuthenticatedAt.Unix(), actual.AuthenticatedAt.Unix()) + assert.Equal(t, expected.IssuedAt.Unix(), actual.IssuedAt.Unix()) + }) + + t.Run("case=delete session", func(t *testing.T) { + var expected Session + require.NoError(t, faker.FakeData(&expected)) + require.NoError(t, p.CreateIdentity(context.Background(), expected.Identity)) + require.NoError(t, p.CreateSession(context.Background(), &expected)) + + require.NoError(t, p.DeleteSession(context.Background(), expected.ID)) + _, err := p.GetSession(context.Background(), expected.ID) + require.Error(t, err) + }) + } +} diff --git a/session/registry.go b/session/registry.go deleted file mode 100644 index 774e88c0635..00000000000 --- a/session/registry.go +++ /dev/null @@ -1,20 +0,0 @@ -package session - -import ( - "time" - - "github.com/gorilla/sessions" - - "github.com/ory/kratos/identity" -) - -type Registry interface { - CookieManager() sessions.Store - SessionManager() Manager - identity.PoolProvider -} - -type Configuration interface { - SessionLifespan() time.Duration - SessionSecrets() [][]byte -} diff --git a/session/session.go b/session/session.go index fc1f3bd829e..3a501630fbb 100644 --- a/session/session.go +++ b/session/session.go @@ -4,44 +4,47 @@ import ( "net/http" "time" - "github.com/google/uuid" + "github.com/gofrs/uuid" "github.com/ory/kratos/identity" + "github.com/ory/kratos/x" ) type Session struct { - SID string `json:"sid"` - ExpiresAt time.Time `json:"expires_at" faker:"time_type"` - AuthenticatedAt time.Time `json:"authenticated_at" faker:"time_type"` - IssuedAt time.Time `json:"issued_at" faker:"time_type"` - Identity *identity.Identity `json:"identity"` - // Devices []Device `json:"devices,omitempty" faker:"-"` + ID uuid.UUID `json:"sid" faker:"uuid" db:"id"` + ExpiresAt time.Time `json:"expires_at" db:"expires_at" faker:"time_type"` + AuthenticatedAt time.Time `json:"authenticated_at" db:"authenticated_at" faker:"time_type"` + IssuedAt time.Time `json:"issued_at" db:"issued_at" faker:"time_type"` + Identity *identity.Identity `json:"identity" faker:"identity" db:"-" belongs_to:"identities" fk_id:"IdentityID"` - modifiedIdentity bool + // IdentityID is a helper struct field for gobuffalo.pop. + IdentityID uuid.UUID `json:"-" faker:"-" db:"identity_id"` + // CreatedAt is a helper struct field for gobuffalo.pop. + CreatedAt time.Time `json:"-" faker:"-" db:"created_at"` + // UpdatedAt is a helper struct field for gobuffalo.pop. + UpdatedAt time.Time `json:"-" faker:"-" db:"updated_at"` + + modifiedIdentity bool `faker:"-" db:"-"` +} + +func (s Session) TableName() string { + return "sessions" } -func NewSession(i *identity.Identity, r *http.Request, c Configuration) *Session { +func NewSession(i *identity.Identity, r *http.Request, c interface { + SessionLifespan() time.Duration +}) *Session { return &Session{ - SID: uuid.New().String(), + ID: x.NewUUID(), ExpiresAt: time.Now().UTC().Add(c.SessionLifespan()), IssuedAt: time.Now().UTC(), Identity: i, - // Devices: []Device{ - // { - // // IP: r.RemoteAddr, - // UserAgent: r.UserAgent(), - // SeenAt: []time.Time{ - // time.Now().UTC(), - // }, - // }, - // }, } } type Device struct { - UserAgent string `json:"user_agent"` - // IP string `json:"ip"` - SeenAt []time.Time `json:"seen_at" faker:"time_types"` + UserAgent string `json:"user_agent"` + SeenAt []time.Time `json:"seen_at" faker:"time_types"` } func (s *Session) UpdateIdentity(i *identity.Identity) *Session { diff --git a/swagger_meta.go b/swagger_meta.go new file mode 100644 index 00000000000..20900830250 --- /dev/null +++ b/swagger_meta.go @@ -0,0 +1,24 @@ +// Ory Kratos +// +// Welcome to the ORY Kratos HTTP API documentation! +// +// Schemes: http, https +// Host: +// BasePath: / +// Version: latest +// +// Consumes: +// - application/json +// - application/x-www-form-urlencoded +// +// Produces: +// - application/json +// +// Extensions: +// --- +// x-request-id: string +// x-forwarded-proto: string +// --- +// +// swagger:meta +package main diff --git a/doc.go b/swagger_types_global.go similarity index 71% rename from doc.go rename to swagger_types_global.go index 8384c5051ff..66c782941c6 100644 --- a/doc.go +++ b/swagger_types_global.go @@ -1,26 +1,3 @@ -// Ory Kratos -// -// Welcome to the ORY Kratos HTTP API documentation! -// -// Schemes: http, https -// Host: -// BasePath: / -// Version: latest -// -// Consumes: -// - application/json -// - application/x-www-form-urlencoded -// -// Produces: -// - application/json -// -// Extensions: -// --- -// x-request-id: string -// x-forwarded-proto: string -// --- -// -// swagger:meta package main // Error response diff --git a/swagger_types_overrides.go b/swagger_types_overrides.go new file mode 100644 index 00000000000..54b166d9d5b --- /dev/null +++ b/swagger_types_overrides.go @@ -0,0 +1,7 @@ +package main + +import "github.com/go-openapi/strfmt" + +// swagger:model UUID +// nolint:deadcode,unused +type uuid strfmt.UUID4 diff --git a/x/body_decoder.go b/x/body_decoder.go deleted file mode 100644 index 78babadde98..00000000000 --- a/x/body_decoder.go +++ /dev/null @@ -1,135 +0,0 @@ -package x - -import ( - "bytes" - "encoding/json" - "mime" - "net/http" - "net/url" - "strconv" - "strings" - - "github.com/pkg/errors" - "github.com/tidwall/sjson" - - "github.com/ory/herodot" - "github.com/ory/x/jsonx" -) - -type BodyDecoder struct{} - -type BodyDecoderOptions struct { - AssertTypesForPrefix string -} - -func NewBodyDecoder() *BodyDecoder { - return &BodyDecoder{} -} - -func (d BodyDecoder) json(r *http.Request) (json.RawMessage, error) { - var p json.RawMessage - if err := jsonx.NewStrictDecoder(r.Body).Decode(&p); err != nil { - return nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP json body: %s", err.Error())) - } - - return p, nil -} - -func (d *BodyDecoder) DecodeForm(form url.Values, o interface{}, opts BodyDecoderOptions) (err error) { - payload, err := d.form(form, opts) - if err != nil { - return err - } - - // This must not be a strict decoder - return errors.WithStack(json.NewDecoder(bytes.NewBuffer(payload)).Decode(o)) -} - -func (d *BodyDecoder) ParseFormFieldOr(values []string, fallback interface{}) (typed interface{}) { - out, err := d.ParseFormField(values) - if err != nil { - return fallback - } - return out -} - -func (d *BodyDecoder) ParseFormField(values []string) (typed interface{}, err error) { - if len(values) == 0 { - return nil, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Values must have at least one element but got none.")) - } - - value := values[0] - // This handles the special case of checkboxes: - // - // - // - if len(values) > 1 { - value = values[len(values)-1] - } - typed = value - if IsValidNumber(value) { - typed, err = strconv.ParseInt(value, 10, 64) - if err != nil { - typed, err = strconv.ParseFloat(value, 64) - if err != nil { - return nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse number: %s", err.Error())) - } - } - } else if strings.ToLower(value) == "true" || strings.ToLower(value) == "false" { - typed, err = strconv.ParseBool(value) - if err != nil { - return nil, errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse bool: %s", err.Error())) - } - } - - return typed, err -} - -func (d *BodyDecoder) form(form url.Values, opts BodyDecoderOptions) (json.RawMessage, error) { - var err error - payload := []byte("{}") - for k := range form { - var typed interface{} = form.Get(k) - - if len(opts.AssertTypesForPrefix) == 0 || strings.HasPrefix(k, opts.AssertTypesForPrefix) { - if typed, err = d.ParseFormField(form[k]); err != nil { - return nil, err - } - } - - payload, err = sjson.SetBytes(payload, k, typed) - if err != nil { - return nil, errors.WithStack(err) - } - } - - return payload, nil -} - -func (d *BodyDecoder) Decode(r *http.Request, o interface{}, opts BodyDecoderOptions) (err error) { - ct, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - if err != nil { - return errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP request content type: %s", err.Error())) - } - - var p json.RawMessage - if ct == "application/json" { - p, err = d.json(r) - } else { - if err := r.ParseForm(); err != nil { - return errors.WithStack(herodot.ErrBadRequest.WithDebug(err.Error()).WithReasonf("Unable to parse HTTP form request: %s", err.Error())) - } - p, err = d.form(r.PostForm, opts) - } - - if err != nil { - return err - } - - // This must not be a strict decoder - if err := json.NewDecoder(bytes.NewBuffer(p)).Decode(o); err != nil { - return errors.WithStack(err) - } - - return nil -} diff --git a/x/body_decoder_test.go b/x/body_decoder_test.go deleted file mode 100644 index 974a4d1a390..00000000000 --- a/x/body_decoder_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package x - -import ( - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestNewBodyDecoder(t *testing.T) { - dec := NewBodyDecoder() - - t.Run("type=form", func(t *testing.T) { - for k, tc := range []struct { - d string - payload url.Values - raw string - result string - opt BodyDecoderOptions - }{ - { - d: "should work with nested keys", - payload: url.Values{ - "traits.foo": {"bar"}, - "request": {"bar"}, - }, - result: `{"request":"bar","traits":{"foo":"bar"}}`, - }, - { - d: "should ignore unless prefix is set", - payload: url.Values{ - "traits.foo": {"12345"}, - "password": {"12345"}, - }, - opt: BodyDecoderOptions{AssertTypesForPrefix: "traits."}, - result: `{"password":"12345","traits":{"foo":12345}}`, - }, - { - d: "should work with true and false", - raw: "traits.consent.newsletter=false&traits.consent.newsletter=true&traits.consent.tos=false", - result: `{"traits":{"consent":{"newsletter":true,"tos":false}}}`, - }, - } { - t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var result json.RawMessage - require.NoError(t, dec.Decode(r, &result, tc.opt)) - require.JSONEq(t, tc.result, string(result), "%s", result) - })) - defer ts.Close() - - var res *http.Response - var err error - - if tc.raw != "" { - res, err = ts.Client().Post(ts.URL, "application/x-www-form-urlencoded", strings.NewReader(tc.raw)) - } else { - res, err = ts.Client().PostForm(ts.URL, tc.payload) - } - - require.NoError(t, err) - require.NoError(t, res.Body.Close()) - }) - } - }) - - t.Run("type=json", func(t *testing.T) { - for k, tc := range []struct { - d string - payload string - result string - }{ - { - d: "should work with nested keys", - payload: `{"request":"bar","traits":{"foo":"bar"}}`, - result: `{"request":"bar","traits":{"foo":"bar"}}`, - }, - } { - t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var result json.RawMessage - require.NoError(t, dec.Decode(r, &result, BodyDecoderOptions{})) - require.JSONEq(t, tc.result, string(result), "%s", result) - })) - defer ts.Close() - - res, err := ts.Client().Post(ts.URL, "application/json", bytes.NewBufferString(tc.payload)) - require.NoError(t, err) - require.NoError(t, res.Body.Close()) - }) - } - - }) -} diff --git a/x/cookie.go b/x/cookie.go index 85d27354fb2..e818d1d6b54 100644 --- a/x/cookie.go +++ b/x/cookie.go @@ -16,11 +16,7 @@ func SessionPersistValues(w http.ResponseWriter, r *http.Request, s sessions.Sto cookie.Values[k] = v } - if err := cookie.Save(r, w); err != nil { - return errors.WithStack(err) - } - - return nil + return errors.WithStack(cookie.Save(r, w)) } // SessionGetString returns a string for the given id and key or an error if the session is invalid, diff --git a/x/time.go b/x/time.go new file mode 100644 index 00000000000..efdcdbb80b4 --- /dev/null +++ b/x/time.go @@ -0,0 +1,17 @@ +package x + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func AssertEqualTime(t *testing.T, expected, actual time.Time) { + assert.EqualValues(t, expected.UTC().Round(time.Second), actual.UTC().Round(time.Second)) +} + +func RequireEqualTime(t *testing.T, expected, actual time.Time) { + require.EqualValues(t, expected.UTC().Round(time.Second), actual.UTC().Round(time.Second)) +} diff --git a/x/uuid.go b/x/uuid.go new file mode 100644 index 00000000000..1394058b9bd --- /dev/null +++ b/x/uuid.go @@ -0,0 +1,21 @@ +package x + +import ( + db "github.com/gofrs/uuid" + "github.com/google/uuid" +) + +var EmptyUUID db.UUID + +func NewUUID() db.UUID { + return db.UUID(uuid.New()) +} + +func ParseUUID(in string) db.UUID { + id, _ := uuid.Parse(in) + return db.UUID(id) +} + +func IsZeroUUID(id db.UUID) bool { + return id == db.UUID{} +} diff --git a/x/uuid_test.go b/x/uuid_test.go new file mode 100644 index 00000000000..806cef5c4ba --- /dev/null +++ b/x/uuid_test.go @@ -0,0 +1,14 @@ +package x + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestUUID(t *testing.T) { + assert.True(t, IsZeroUUID(ParseUUID("foo"))) + assert.True(t, IsZeroUUID(ParseUUID(""))) + assert.True(t, IsZeroUUID(ParseUUID("asfdt4ifgdsl"))) + assert.False(t, IsZeroUUID(NewUUID())) +}