From 215e13b6595a345d027a91feeb201a0e5230c4e0 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Sun, 1 Dec 2019 21:22:15 +0100 Subject: [PATCH] Replace DBAL layer with gobuffalo/pop (#130) This is a major refactoring of the internal DBAL. After a successful proof of concept and evaluation of gobuffalo/pop, we believe this to be the best DBAL for Go at the moment. It abstracts a lot of boilerplate code away. As with all sophisticated DBALs, pop too has its quirks. There are several issues that have been discovered during testing and adoption: https://github.com/gobuffalo/pop/issues/136 https://github.com/gobuffalo/pop/issues/476 https://github.com/gobuffalo/pop/issues/473 https://github.com/gobuffalo/pop/issues/469 https://github.com/gobuffalo/pop/issues/466 However, the upside of moving much of the hard database/sql plumbing into another library cleans up the code base significantly and reduces complexity. As part of this change, the "ephermal" DBAL ("in memory") will be removed and sqlite will be used instead. This further reduces complexity of the code base and code-duplication. To support sqlite, CGO is required, which means that we need to run tests with `go test -tags sqlite` on a machine that has g++ installed. This also means that we need a Docker Image with `alpine` as opposed to pure `scratch`. While this is certainly a downside, the upside of less maintenance and "free" support for SQLite, PostgreSQL, MySQL, and CockroachDB simply outweighs any downsides that come with CGO. --- .circleci/config.yml | 76 ++-- .dockerignore | 1 + .golangci.yml | 14 + Dockerfile | 4 - Makefile | 2 +- README.md | 1 - cmd/client/migrate.go | 102 ++--- cmd/serve.go | 2 +- contrib/sql/.soda.yml | 14 + contrib/sql/README.md | 25 ++ .../20191120000000_identities.down.fizz | 1 + .../20191120000000_identities.up.fizz | 41 ++ .../20191121000000_requests.down.fizz | 5 + .../20191121000000_requests.up.fizz | 55 +++ .../20191122000000_sessions.down.fizz | 1 + .../20191122000000_sessions.up.fizz | 11 + .../20191123000000_errors.down.fizz | 1 + .../migrations/20191123000000_errors.up.fizz | 8 + .../20191130170530_identities.mysql.down.sql | 0 .../20191130170530_identities.mysql.up.sql | 1 + contrib/sql/migrations/postgres/1.sql | 61 --- contrib/sql/migrations/postgres/2.sql | 12 - contrib/sql/migrations/tests/1_test.sql | 20 - contrib/sql/migrations/tests/2_test.sql | 36 -- contrib/swagutil/cmd/sanitize.go | 2 + docs/api.swagger.json | 88 ++--- driver/configuration/provider_viper_test.go | 5 +- driver/driver_default.go | 17 +- driver/registry.go | 5 + driver/registry_abstract.go | 318 --------------- driver/registry_default.go | 372 ++++++++++++++++++ driver/registry_default_hooks.go | 70 ++++ ...act_login.go => registry_default_login.go} | 16 +- ...on.go => registry_default_registration.go} | 24 +- driver/registry_memory.go | 87 ---- driver/registry_sql.go | 205 ---------- driver/registry_sql_test.go | 62 --- driver/sql_migration_files.go | 352 ----------------- go.mod | 28 +- go.sum | 210 +++++++++- identity/credentials.go | 89 ++++- identity/extension.go | 2 +- identity/extension_test.go | 3 +- identity/handler.go | 25 +- identity/handler_test.go | 50 +-- identity/identity.go | 137 +++++-- identity/identity_test.go | 4 +- identity/pool.go | 356 ++++++++++++++--- identity/pool_memory.go | 199 ---------- identity/pool_sql.go | 282 ------------- identity/pool_test.go | 269 ------------- identity/validator.go | 5 +- identity/validator_test.go | 17 +- internal/driver.go | 40 +- internal/faker.go | 31 +- persistence/aliases/http_header.go | 16 + persistence/aliases/scanner_json.go | 36 ++ persistence/reference.go | 31 ++ persistence/sql/persister.go | 76 ++++ persistence/sql/persister_errorx.go | 118 ++++++ persistence/sql/persister_identity.go | 255 ++++++++++++ persistence/sql/persister_login.go | 48 +++ persistence/sql/persister_profile.go | 30 ++ persistence/sql/persister_registration.go | 46 +++ persistence/sql/persister_session.go | 29 ++ persistence/sql/persister_test.go | 138 +++++++ persistence/sql/stub/identity-2.schema.json | 11 + persistence/sql/stub/identity.schema.json | 21 + sdk/go/kratos/models/identity.go | 55 +-- sdk/go/kratos/models/identity_credentials.go | 79 ---- sdk/go/kratos/models/login_request.go | 26 +- sdk/go/kratos/models/login_request_method.go | 22 -- .../models/profile_management_request.go | 26 +- sdk/go/kratos/models/registration_request.go | 26 +- .../models/registration_request_method.go | 22 -- .../models/request_method_configurator.go | 10 + .../models/{c_s_r_f_setter.go => traits.go} | 6 +- sdk/go/kratos/models/uuid.go | 31 ++ selfservice/errorx/handler.go | 4 +- selfservice/errorx/handler_test.go | 10 +- selfservice/errorx/helper.go | 6 +- selfservice/errorx/manager.go | 93 +---- selfservice/errorx/manager_memory.go | 92 ----- selfservice/errorx/manager_sql.go | 113 ------ selfservice/errorx/manager_test.go | 84 ---- selfservice/errorx/persistence.go | 76 ++++ selfservice/flow/login/error.go | 9 +- selfservice/flow/login/handler.go | 12 +- selfservice/flow/login/handler_test.go | 8 +- selfservice/flow/login/hook.go | 2 +- selfservice/flow/login/hook_test.go | 14 +- selfservice/flow/login/persistence.go | 151 ++++--- selfservice/flow/login/request.go | 96 +++-- selfservice/flow/login/request_method.go | 84 ++++ selfservice/flow/login/request_test.go | 13 +- selfservice/flow/logout/handler.go | 2 +- selfservice/flow/logout/handler_test.go | 11 +- selfservice/flow/profile/error.go | 11 +- selfservice/flow/profile/handler.go | 62 +-- selfservice/flow/profile/handler_test.go | 16 +- selfservice/flow/profile/persistence.go | 110 +++++- selfservice/flow/profile/request.go | 40 +- selfservice/flow/profile/request_test.go | 15 +- selfservice/flow/registration/error.go | 9 +- selfservice/flow/registration/handler.go | 12 +- selfservice/flow/registration/handler_test.go | 8 +- selfservice/flow/registration/hook.go | 11 +- selfservice/flow/registration/hook_test.go | 13 +- selfservice/flow/registration/persistence.go | 153 ++++--- selfservice/flow/registration/request.go | 95 +++-- .../flow/registration/request_method.go | 85 ++++ selfservice/flow/registration/request_test.go | 13 +- selfservice/form/html_form.go | 25 +- selfservice/hook/redirector_test.go | 5 +- selfservice/hook/session_issuer.go | 5 +- selfservice/hook/session_issuer_test.go | 26 +- selfservice/persistence/ephermal.go | 149 ------- selfservice/persistence/persister.go | 13 - selfservice/persistence/postgresql.go.bak | 220 ----------- selfservice/persistence/prototypes.go | 56 --- .../persistence/prototypes_test.go.bak | 1 - .../persistence/request_manager_test.go.bak | 110 ------ .../strategy/oidc/provider_config_test.go | 2 +- selfservice/strategy/oidc/strategy.go | 47 ++- .../strategy/oidc/strategy_helper_test.go | 2 +- selfservice/strategy/oidc/strategy_test.go | 58 +-- selfservice/strategy/oidc/types.go | 4 +- selfservice/strategy/password/login.go | 16 +- selfservice/strategy/password/login_test.go | 192 ++++----- selfservice/strategy/password/registration.go | 18 +- .../strategy/password/registration_test.go | 129 +++--- .../strategy/password/strategy_test.go | 4 +- session/handler.go | 38 +- session/handler_mock.go | 37 +- session/handler_test.go | 16 +- session/manager.go | 100 +---- session/manager_http.go | 105 +++++ session/manager_memory.go | 55 --- session/manager_sql.go | 96 ----- session/manager_test.go | 213 ---------- session/persistence.go | 79 ++++ session/registry.go | 20 - session/session.go | 47 +-- swagger_meta.go | 24 ++ doc.go => swagger_types_global.go | 23 -- swagger_types_overrides.go | 7 + x/body_decoder.go | 135 ------- x/body_decoder_test.go | 100 ----- x/cookie.go | 6 +- x/time.go | 17 + x/uuid.go | 21 + x/uuid_test.go | 14 + 152 files changed, 4029 insertions(+), 4761 deletions(-) create mode 100644 contrib/sql/.soda.yml create mode 100644 contrib/sql/README.md create mode 100644 contrib/sql/migrations/20191120000000_identities.down.fizz create mode 100644 contrib/sql/migrations/20191120000000_identities.up.fizz create mode 100644 contrib/sql/migrations/20191121000000_requests.down.fizz create mode 100644 contrib/sql/migrations/20191121000000_requests.up.fizz create mode 100644 contrib/sql/migrations/20191122000000_sessions.down.fizz create mode 100644 contrib/sql/migrations/20191122000000_sessions.up.fizz create mode 100644 contrib/sql/migrations/20191123000000_errors.down.fizz create mode 100644 contrib/sql/migrations/20191123000000_errors.up.fizz create mode 100644 contrib/sql/migrations/20191130170530_identities.mysql.down.sql create mode 100644 contrib/sql/migrations/20191130170530_identities.mysql.up.sql delete mode 100644 contrib/sql/migrations/postgres/1.sql delete mode 100644 contrib/sql/migrations/postgres/2.sql delete mode 100644 contrib/sql/migrations/tests/1_test.sql delete mode 100644 contrib/sql/migrations/tests/2_test.sql delete mode 100644 driver/registry_abstract.go create mode 100644 driver/registry_default.go create mode 100644 driver/registry_default_hooks.go rename driver/{registry_abstract_login.go => registry_default_login.go} (56%) rename driver/{registry_abstract_registration.go => registry_default_registration.go} (59%) delete mode 100644 driver/registry_memory.go delete mode 100644 driver/registry_sql.go delete mode 100644 driver/registry_sql_test.go delete mode 100644 driver/sql_migration_files.go delete mode 100644 identity/pool_memory.go delete mode 100644 identity/pool_sql.go delete mode 100644 identity/pool_test.go create mode 100644 persistence/aliases/http_header.go create mode 100644 persistence/aliases/scanner_json.go create mode 100644 persistence/reference.go create mode 100644 persistence/sql/persister.go create mode 100644 persistence/sql/persister_errorx.go create mode 100644 persistence/sql/persister_identity.go create mode 100644 persistence/sql/persister_login.go create mode 100644 persistence/sql/persister_profile.go create mode 100644 persistence/sql/persister_registration.go create mode 100644 persistence/sql/persister_session.go create mode 100644 persistence/sql/persister_test.go create mode 100644 persistence/sql/stub/identity-2.schema.json create mode 100644 persistence/sql/stub/identity.schema.json delete mode 100644 sdk/go/kratos/models/identity_credentials.go create mode 100644 sdk/go/kratos/models/request_method_configurator.go rename sdk/go/kratos/models/{c_s_r_f_setter.go => traits.go} (69%) create mode 100644 sdk/go/kratos/models/uuid.go delete mode 100644 selfservice/errorx/manager_memory.go delete mode 100644 selfservice/errorx/manager_sql.go delete mode 100644 selfservice/errorx/manager_test.go create mode 100644 selfservice/errorx/persistence.go create mode 100644 selfservice/flow/login/request_method.go create mode 100644 selfservice/flow/registration/request_method.go delete mode 100644 selfservice/persistence/ephermal.go delete mode 100644 selfservice/persistence/persister.go delete mode 100644 selfservice/persistence/postgresql.go.bak delete mode 100644 selfservice/persistence/prototypes.go delete mode 100644 selfservice/persistence/prototypes_test.go.bak delete mode 100644 selfservice/persistence/request_manager_test.go.bak create mode 100644 session/manager_http.go delete mode 100644 session/manager_memory.go delete mode 100644 session/manager_sql.go delete mode 100644 session/manager_test.go create mode 100644 session/persistence.go delete mode 100644 session/registry.go create mode 100644 swagger_meta.go rename doc.go => swagger_types_global.go (71%) create mode 100644 swagger_types_overrides.go delete mode 100644 x/body_decoder.go delete mode 100644 x/body_decoder_test.go create mode 100644 x/time.go create mode 100644 x/uuid.go create mode 100644 x/uuid_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index c8bbf8141fb6..1920d8183ca3 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 1e0e17736b40..610e025e88b0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ tmp scripts .idea .git/ +database.yaml diff --git a/.golangci.yml b/.golangci.yml index e34a91c04fc9..01c76d54eced 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 e55fca408ddb..b3ab9a4c014e 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 065052c27bfa..8870c993dba4 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 63d65ac86b8d..abb8293bb642 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 ddfdcda985c8..31d297be969e 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 f24049839fb8..26bafbde37e2 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 000000000000..f265223e605a --- /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 000000000000..a6d9fa6684e8 --- /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 000000000000..8a87d19060fd --- /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 000000000000..3d06612a898f --- /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 000000000000..6d1b92dfb337 --- /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 000000000000..05f5105bb46a --- /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 000000000000..dc5c982c81fe --- /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 000000000000..94ab9a4e1435 --- /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 000000000000..9ada90a727b8 --- /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 000000000000..e17347c2692a --- /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 000000000000..e69de29bb2d1 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 000000000000..8069ee98f315 --- /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 7d83925c0ba5..000000000000 --- 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 cfd157adbc7e..000000000000 --- 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 0da70a54332f..000000000000 --- 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 8db850194810..000000000000 --- 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 02f473b6fdf4..0c37a133a3ea 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 4d36bb7ce444..e3642998b56b 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 33bb4396bc92..b4af1cde42f4 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 e8114fa1f944..0f53148f1e42 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 cd0de5dc8cda..24f6a5db95eb 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 8b7c9e8b619a..000000000000 --- 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 000000000000..cbcb704c8111 --- /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 000000000000..13a664ef1d10 --- /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 8becbef0b78e..a434d178e4a6 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 291f5cc6290c..87b7730422f7 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 ff55151627ee..000000000000 --- 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 1d6d57136f1d..000000000000 --- 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 7ff8f420d93f..000000000000 --- 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 629910c7215d..000000000000 --- 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 8d943dc57087..c3be82d25679 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 ca5c3df5b17c..e4f887a1fd90 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 32f6d49ab8b5..5053461cfc67 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 686662b866db..c29ae6487174 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 426bb4f4e02a..dda1f6797a07 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 dd7899b832bf..b67d85f82f2e 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 51d2d75d438c..5c7b215a7015 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 1ca8fd573663..3d6eb8aa19cb 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 3d0d73e50c43..f49954b4055c 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 c824e403ee54..422beb89a90d 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 4f9350470fbe..000000000000 --- 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 1f54767e3764..000000000000 --- 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 7f415660fcd0..000000000000 --- 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 ce2c936aac73..10539f313c2e 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 843db0c33c5d..1214fca49225 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 801b96c002c3..00874cfd0cb9 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 19d788b416ae..65da6c6dc578 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 000000000000..2cb0c53c94b0 --- /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 000000000000..219aba19b276 --- /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 000000000000..e433fe097e79 --- /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 000000000000..cc2c8ab623b0 --- /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 000000000000..1e45f4b3eba2 --- /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 000000000000..b8a1d653b5e6 --- /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 000000000000..b78eef04ddcf --- /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 000000000000..0ac1f751d7e4 --- /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 000000000000..92b495fdc2f6 --- /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 000000000000..1eb0ce09bf8d --- /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 000000000000..fd57b6068e13 --- /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 000000000000..ec435cd1a957 --- /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 000000000000..fe94126c9526 --- /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 b9f5cb387a42..09dce7e70431 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 2df940ceb8d0..000000000000 --- 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 d974a3652924..70de61bff636 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 c9938f84d55f..ff13f554ee12 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 518b58df8608..816c8afb917f 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 c927b412af92..c360d5ec1994 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 f4c89c6ff760..9a563398e79a 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 000000000000..687449372af5 --- /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 9f48befa1a04..e39059ab688c 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 000000000000..0a86f483b5fb --- /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 b466f4dc37dc..493dcbe327d5 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 a58d4fe32fc4..c74609fcdce1 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 eb38aa9c9065..6eb8acf56007 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 29013dad8109..2d5668feeceb 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 53b0966eb5dc..000000000000 --- 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 61ce5c1e9734..000000000000 --- 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 c30cec190725..000000000000 --- 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 000000000000..fb0faae0944d --- /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 8deece8004bc..8ccd3d0aa092 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 c9c10f45fe0d..73aa5c431298 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 530fdd873939..a62776944dba 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 40588b2e94d8..ffbbf0e7d70d 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 27d4d3f9a2fb..61d8447be9a9 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 205bc85e4b17..90cedfd3e763 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 692e57b20e2f..c46ecf236628 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 000000000000..56bb4e6ee137 --- /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 e9313c52ff20..ba7bd2495d68 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 3d7f590211b1..43807cb94195 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 26168464de20..c39d7ec42a0c 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 27f0cab6a20c..68b4e0d43859 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 baea00473889..a50c9a0793e7 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 5a4dd66ecedd..75434a6689b1 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 fcb2397eaf59..b92d1407db71 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 ebac5ea5255f..5ce15c0007f8 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 6a97a54508fa..32f8915f228c 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 4431fcf02154..e2c0c736b0bf 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 1a90082478ae..7b0574ed3989 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 78ff16c09127..f3b01b09bebb 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 026a7bdcfc72..5ac8b1c3fce9 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 0ac3225f78ea..68e762dba265 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 6d01c5b1f5ed..bcb53d28ebc9 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 24a4464ae600..bb270e013617 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 000000000000..0f4cf8ad1c65 --- /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 d3718dedcc08..0abd3331b2fb 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 d59c997b7e7c..241e0df1da2c 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 885e9ca77c0a..7efbce847bf2 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 08298e3d1169..52f911faf6fd 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 8fe3aaec65e6..869c5499a524 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 a66d27c20f9e..000000000000 --- 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 7c80c2828bad..000000000000 --- 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 8b7ec7236fa4..000000000000 --- 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 84a0f0548b01..000000000000 --- 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 dc7cf831f60f..000000000000 --- 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 fddd6ee26665..000000000000 --- 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 d8bbdebc9d8d..0fd687dba748 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 981008fd0d73..8323bb067804 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 f38bbf7ae9f2..773017150a3b 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 df0dae3e0ad7..e83d00213fc9 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 35470cc028a4..17b586b6d1c3 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 cd20b2c32fdf..4404e4336791 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 dad30aa00ef7..ea188e5397f2 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 54d51b3cb4b7..958b1cb28d04 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 5c98ffc3c4db..361583410918 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 13b95f2dcfb3..ae4645fd5c64 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 e6066f06a60b..798746eb6a26 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 0f06f3ab32b7..9581408a792a 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 827c372cf5a9..5b8cb337aae7 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 16f0711eba68..aa6a8815fcb3 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 000000000000..85852c6c57e3 --- /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 d2a8632a9af4..000000000000 --- 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 7190f7c90fd6..000000000000 --- 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 5e77ccff3161..000000000000 --- 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 000000000000..16d39829c043 --- /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 774e88c06352..000000000000 --- 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 fc1f3bd829ec..3a501630fbba 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 000000000000..20900830250f --- /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 8384c5051ff3..66c782941c67 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 000000000000..54b166d9d5be --- /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 78babadde982..000000000000 --- 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 974a4d1a390a..000000000000 --- 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 85d27354fb26..e818d1d6b548 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 000000000000..efdcdbb80b4d --- /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 000000000000..1394058b9bdf --- /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 000000000000..806cef5c4ba8 --- /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())) +}