diff --git a/.releaserc.js b/.releaserc.js index c5503ecea1..f24249cada 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -1,7 +1,7 @@ module.exports = { branches: [ {name: 'main'}, - {name: 'next'} + {name: 'next'}, ], plugins: [ "@semantic-release/commit-analyzer" diff --git a/build/grpc/Dockerfile b/build/grpc/Dockerfile index aa1727ef8c..d6b86dd01d 100644 --- a/build/grpc/Dockerfile +++ b/build/grpc/Dockerfile @@ -16,11 +16,11 @@ ENV PROTOC_ARCH x86_64 ## protoc and protoc-gen-grpc-web for later use ####################### FROM ${BUILDARCH}-base -ARG PROTOC_VERSION=3.18.0 +ARG PROTOC_VERSION=22.3 ARG PROTOC_ZIP=protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip ARG GRPC_WEB_VERSION=1.3.0 -ARG GATEWAY_VERSION=2.15.1 -ARG VALIDATOR_VERSION=0.6.2 +ARG GATEWAY_VERSION=2.15.2 +ARG VALIDATOR_VERSION=0.10.1 # no arm specific version available and x86 works fine at the moment: ARG GRPC_WEB=protoc-gen-grpc-web-${GRPC_WEB_VERSION}-linux-x86_64 diff --git a/build/zitadel/Dockerfile b/build/zitadel/Dockerfile index 1ed0e34d7b..7c0f7bdcad 100644 --- a/build/zitadel/Dockerfile +++ b/build/zitadel/Dockerfile @@ -73,7 +73,6 @@ COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/statik/statik.go COPY --from=go-stub /go/src/github.com/zitadel/zitadel/pkg/grpc pkg/grpc COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/v2/zitadel openapi/v2/zitadel COPY --from=go-stub /go/src/github.com/zitadel/zitadel/openapi/statik/statik.go openapi/statik/statik.go -COPY --from=go-stub /go/src/github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/templates.gen.go internal/protoc/protoc-gen-authoption/templates.gen.go COPY --from=go-stub /go/src/github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption/options.pb.go internal/protoc/protoc-gen-authoption/authoption/options.pb.go COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/proto docs/docs/apis/proto COPY --from=go-stub /go/src/github.com/zitadel/zitadel/docs/apis/assets docs/docs/apis/assets diff --git a/build/zitadel/generate-grpc.sh b/build/zitadel/generate-grpc.sh index 02d1fa86b1..6f85200da3 100755 --- a/build/zitadel/generate-grpc.sh +++ b/build/zitadel/generate-grpc.sh @@ -15,17 +15,11 @@ protoc \ -I=/proto/include/ \ --go_out $GOPATH/src \ --go-grpc_out $GOPATH/src \ + --validate_out=lang=go:${GOPATH}/src \ $(find ${PROTO_PATH} -iname *.proto) -# generate authoptions code from templates -go-bindata \ - -pkg main \ - -prefix internal/protoc/protoc-gen-authoption \ - -o ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption/templates.gen.go \ - ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption/templates - # install authoption proto compiler -go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-authoption +go install ${ZITADEL_PATH}/internal/protoc/protoc-gen-auth # output folder for openapi v2 mkdir -p ${OPENAPI_PATH} @@ -39,28 +33,20 @@ protoc \ --grpc-gateway_opt logtostderr=true \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ - --authoption_out ${GRPC_PATH}/system \ + --auth_out ${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/system.proto -# authoptions are generated into the wrong folder -mv ${ZITADEL_PATH}/pkg/grpc/system/zitadel/* ${ZITADEL_PATH}/pkg/grpc/system -rm -r ${ZITADEL_PATH}/pkg/grpc/system/zitadel - protoc \ -I=/proto/include \ --grpc-gateway_out ${GOPATH}/src \ --grpc-gateway_opt logtostderr=true \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ - --authoption_out ${GRPC_PATH}/admin \ + --auth_out ${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/admin.proto -# authoptions are generated into the wrong folder -mv ${ZITADEL_PATH}/pkg/grpc/admin/zitadel/* ${ZITADEL_PATH}/pkg/grpc/admin -rm -r ${ZITADEL_PATH}/pkg/grpc/admin/zitadel - protoc \ -I=/proto/include \ --grpc-gateway_out ${GOPATH}/src \ @@ -69,14 +55,10 @@ protoc \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ --openapiv2_opt allow_delete_body=true \ - --authoption_out ${GRPC_PATH}/management \ + --auth_out ${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/management.proto -# authoptions are generated into the wrong folder -mv ${ZITADEL_PATH}/pkg/grpc/management/zitadel/* ${ZITADEL_PATH}/pkg/grpc/management -rm -r ${ZITADEL_PATH}/pkg/grpc/management/zitadel - protoc \ -I=/proto/include \ --grpc-gateway_out ${GOPATH}/src \ @@ -85,14 +67,10 @@ protoc \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ --openapiv2_opt allow_delete_body=true \ - --authoption_out=${GRPC_PATH}/auth \ + --auth_out=${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/auth.proto -# authoptions are generated into the wrong folder -mv ${ZITADEL_PATH}/pkg/grpc/auth/zitadel/* ${ZITADEL_PATH}/pkg/grpc/auth -rm -r ${ZITADEL_PATH}/pkg/grpc/auth/zitadel - protoc \ -I=/proto/include \ --grpc-gateway_out ${GOPATH}/src \ @@ -101,14 +79,10 @@ protoc \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ --openapiv2_opt allow_delete_body=true \ - --authoption_out=${GRPC_PATH}/user \ + --auth_out=${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/user/v2alpha/user_service.proto -# authoptions are generated into the wrong folder -cp -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel/* ${ZITADEL_PATH}/pkg/grpc -rm -r ${ZITADEL_PATH}/pkg/grpc/user/zitadel - protoc \ -I=/proto/include \ --grpc-gateway_out ${GOPATH}/src \ @@ -117,12 +91,8 @@ protoc \ --openapiv2_out ${OPENAPI_PATH} \ --openapiv2_opt logtostderr=true \ --openapiv2_opt allow_delete_body=true \ - --authoption_out=${GRPC_PATH}/session \ + --auth_out=${GOPATH}/src \ --validate_out=lang=go:${GOPATH}/src \ ${PROTO_PATH}/session/v2alpha/session_service.proto -# authoptions are generated into the wrong folder -cp -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel/* ${ZITADEL_PATH}/pkg/grpc -rm -r ${ZITADEL_PATH}/pkg/grpc/session/zitadel - echo "done generating grpc" diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 8996e384c6..1acbb55d44 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -233,6 +233,8 @@ OIDC: Path: /oidc/v1/end_session Keys: Path: /oauth/v2/keys + DeviceAuth: + Path: /oauth/v2/device_authorization SAML: ProviderConfig: @@ -319,6 +321,8 @@ SystemDefaults: ApplicationKeySize: 2048 Multifactors: OTP: + # If this is empty, the issuer is the requested domain + # This is helpful in scenarios with multiple ZITADEL environments or virtual instances Issuer: "ZITADEL" DomainVerification: VerificationGenerator: @@ -394,6 +398,7 @@ Quotas: Eventstore: PushTimeout: 15s + AllowOrderByCreationDate: false DefaultInstance: InstanceName: diff --git a/cmd/setup/03.go b/cmd/setup/03.go index 3630ac85f6..9774ab7b7b 100644 --- a/cmd/setup/03.go +++ b/cmd/setup/03.go @@ -76,6 +76,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error { nil, nil, nil, + nil, ) if err != nil { diff --git a/cmd/setup/10.go b/cmd/setup/10.go index 4efccc6537..fd8683f49d 100644 --- a/cmd/setup/10.go +++ b/cmd/setup/10.go @@ -13,8 +13,12 @@ import ( ) var ( - //go:embed 10.sql - correctCreationDate10 string + //go:embed 10_create_temp_table.sql + correctCreationDate10CreateTable string + //go:embed 10_fill_table.sql + correctCreationDate10FillTable string + //go:embed 10_update.sql + correctCreationDate10Update string ) type CorrectCreationDate struct { @@ -34,7 +38,17 @@ func (mig *CorrectCreationDate) Execute(ctx context.Context) (err error) { return err } } - res, err := tx.ExecContext(ctx, correctCreationDate10) + _, err := tx.ExecContext(ctx, correctCreationDate10CreateTable) + if err != nil { + return err + } + + _, err = tx.ExecContext(ctx, correctCreationDate10FillTable) + if err != nil { + return err + } + + res, err := tx.ExecContext(ctx, correctCreationDate10Update) if err != nil { return err } diff --git a/cmd/setup/10_create_temp_table.sql b/cmd/setup/10_create_temp_table.sql new file mode 100644 index 0000000000..fb823b2ecf --- /dev/null +++ b/cmd/setup/10_create_temp_table.sql @@ -0,0 +1,6 @@ +CREATE temporary TABLE IF NOT EXISTS wrong_events ( + instance_id TEXT + , event_sequence BIGINT + , current_cd TIMESTAMPTZ + , next_cd TIMESTAMPTZ +); diff --git a/cmd/setup/10.sql b/cmd/setup/10_fill_table.sql similarity index 57% rename from cmd/setup/10.sql rename to cmd/setup/10_fill_table.sql index 591d0b3d88..e62bbd8391 100644 --- a/cmd/setup/10.sql +++ b/cmd/setup/10_fill_table.sql @@ -1,9 +1,4 @@ -CREATE temporary TABLE IF NOT EXISTS wrong_events ( - instance_id TEXT - , event_sequence BIGINT - , current_cd TIMESTAMPTZ - , next_cd TIMESTAMPTZ -); +TRUNCATE wrong_events; INSERT INTO wrong_events ( SELECT * FROM ( @@ -21,6 +16,4 @@ INSERT INTO wrong_events ( current_cd < next_cd ORDER BY event_sequence DESC -); - -UPDATE eventstore.events e SET creation_date = we.next_cd FROM wrong_events we WHERE e.event_sequence = we.event_sequence and e.instance_id = we.instance_id; \ No newline at end of file +); \ No newline at end of file diff --git a/cmd/setup/10_update.sql b/cmd/setup/10_update.sql new file mode 100644 index 0000000000..5254ba8dda --- /dev/null +++ b/cmd/setup/10_update.sql @@ -0,0 +1 @@ +UPDATE eventstore.events e SET creation_date = we.next_cd FROM wrong_events we WHERE e.event_sequence = we.event_sequence and e.instance_id = we.instance_id; diff --git a/cmd/setup/cleanup.go b/cmd/setup/cleanup.go new file mode 100644 index 0000000000..7139b67d35 --- /dev/null +++ b/cmd/setup/cleanup.go @@ -0,0 +1,51 @@ +package setup + +import ( + "context" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/migration" +) + +func NewCleanup() *cobra.Command { + return &cobra.Command{ + Use: "cleanup", + Short: "cleans up migration if they got stuck", + Long: `cleans up migration if they got stuck`, + Run: func(cmd *cobra.Command, args []string) { + config := MustNewConfig(viper.GetViper()) + Cleanup(config) + }, + } +} + +func Cleanup(config *Config) { + ctx := context.Background() + + logging.Info("cleanup started") + + dbClient, err := database.Connect(config.Database, false) + logging.OnError(err).Fatal("unable to connect to database") + + es, err := eventstore.Start(&eventstore.Config{Client: dbClient}) + logging.OnError(err).Fatal("unable to start eventstore") + migration.RegisterMappers(es) + + step, err := migration.LatestStep(ctx, es) + logging.OnError(err).Fatal("unable to query latest migration") + + if step.BaseEvent.EventType != migration.StartedType { + logging.Info("there is no stuck migration please run `zitadel setup`") + return + } + + logging.WithFields("name", step.Name).Info("cleanup migration") + + err = migration.CancelStep(ctx, es, step) + logging.OnError(err).Fatal("cleanup migration failed please retry") +} diff --git a/cmd/setup/config_change.go b/cmd/setup/config_change.go index 71836c11a4..f112dd8c7b 100644 --- a/cmd/setup/config_change.go +++ b/cmd/setup/config_change.go @@ -33,7 +33,8 @@ func (mig *externalConfigChange) Check() bool { } func (mig *externalConfigChange) Execute(ctx context.Context) error { - cmd, err := command.StartCommands(mig.es, + cmd, err := command.StartCommands( + mig.es, systemdefaults.SystemDefaults{}, nil, nil, @@ -50,6 +51,7 @@ func (mig *externalConfigChange) Execute(ctx context.Context) error { nil, nil, nil, + nil, ) if err != nil { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index e90ad85f94..918f08dc2d 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -45,6 +45,8 @@ Requirements: }, } + cmd.AddCommand(NewCleanup()) + Flags(cmd) return cmd diff --git a/cmd/start/start.go b/cmd/start/start.go index f80fc6fef1..e0829b7eb3 100644 --- a/cmd/start/start.go +++ b/cmd/start/start.go @@ -12,14 +12,13 @@ import ( "syscall" "time" - "github.com/zitadel/saml/pkg/provider" - clockpkg "github.com/benbjohnson/clock" "github.com/gorilla/mux" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/zitadel/logging" "github.com/zitadel/oidc/v2/pkg/op" + "github.com/zitadel/saml/pkg/provider" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" @@ -116,7 +115,7 @@ func startZitadel(config *Config, masterKey string) error { return fmt.Errorf("cannot start queries: %w", err) } - authZRepo, err := authz.Start(queries, dbClient, keys.OIDC, config.ExternalSecure) + authZRepo, err := authz.Start(queries, dbClient, keys.OIDC, config.ExternalSecure, config.Eventstore.AllowOrderByCreationDate) if err != nil { return fmt.Errorf("error starting authz repo: %w", err) } @@ -147,6 +146,7 @@ func startZitadel(config *Config, masterKey string) error { keys.OIDC, keys.SAML, &http.Client{}, + authZRepo, ) if err != nil { return fmt.Errorf("cannot start commands: %w", err) @@ -229,11 +229,11 @@ func startAPIs( if err != nil { return fmt.Errorf("error creating api %w", err) } - authRepo, err := auth_es.Start(ctx, config.Auth, config.SystemDefaults, commands, queries, dbClient, eventstore, keys.OIDC, keys.User) + authRepo, err := auth_es.Start(ctx, config.Auth, config.SystemDefaults, commands, queries, dbClient, eventstore, keys.OIDC, keys.User, config.Eventstore.AllowOrderByCreationDate) if err != nil { return fmt.Errorf("error starting auth repo: %w", err) } - adminRepo, err := admin_es.Start(ctx, config.Admin, store, dbClient, eventstore) + adminRepo, err := admin_es.Start(ctx, config.Admin, store, dbClient, eventstore, config.Eventstore.AllowOrderByCreationDate) if err != nil { return fmt.Errorf("error starting admin repo: %w", err) } @@ -249,7 +249,7 @@ func startAPIs( if err := apis.RegisterServer(ctx, auth.CreateServer(commands, queries, authRepo, config.SystemDefaults, keys.User, config.ExternalSecure, config.AuditLogRetention)); err != nil { return err } - if err := apis.RegisterService(ctx, user.CreateServer(commands, queries)); err != nil { + if err := apis.RegisterService(ctx, user.CreateServer(commands, queries, keys.User)); err != nil { return err } if err := apis.RegisterService(ctx, session.CreateServer(commands, queries)); err != nil { @@ -294,6 +294,7 @@ func startAPIs( return fmt.Errorf("unable to start login: %w", err) } apis.RegisterHandlerOnPrefix(login.HandlerPrefix, l.Handler()) + apis.HandleFunc(login.EndpointDeviceAuth, login.RedirectDeviceAuthToPrefix) // handle grpc at last to be able to handle the root, because grpc and gateway require a lot of different prefixes apis.RouteGRPC() diff --git a/console/src/app/pages/projects/apps/app-create/app-create.component.html b/console/src/app/pages/projects/apps/app-create/app-create.component.html index 7bd4729e67..5e50ecb383 100644 --- a/console/src/app/pages/projects/apps/app-create/app-create.component.html +++ b/console/src/app/pages/projects/apps/app-create/app-create.component.html @@ -437,6 +437,7 @@ class="redirect-section" [disabled]="false" [(ngModel)]="redirectUris" + [ngModelOptions]="{ standalone: true }" [getValues]="requestRedirectValuesSubject$" title="{{ 'APP.OIDC.REDIRECT' | translate }}" [isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE" @@ -447,6 +448,7 @@ class="redirect-section" [disabled]="false" [(ngModel)]="postLogoutUrisList" + [ngModelOptions]="{ standalone: true }" title="{{ 'APP.OIDC.POSTLOGOUTREDIRECT' | translate }}" [getValues]="requestRedirectValuesSubject$" [isNative]="appType?.value.oidcAppType === OIDCAppType.OIDC_APP_TYPE_NATIVE" diff --git a/docs/docs/apis/actions/external-authentication.md b/docs/docs/apis/actions/external-authentication.md index 294a99cad0..f26f55af84 100644 --- a/docs/docs/apis/actions/external-authentication.md +++ b/docs/docs/apis/actions/external-authentication.md @@ -2,7 +2,7 @@ title: External Authentication Flow --- -This flow is executed if the user logs in using an [identity provider](/guides/integrate/identity-providers/introduction.md) or using a [jwt token](/concepts/structure/jwt_idp). +This flow is executed if the user logs in using an [identity provider](/guides/integrate/identity-providers) or using a [jwt token](/concepts/structure/jwt_idp). ## Post Authentication diff --git a/docs/docs/apis/actions/objects.md b/docs/docs/apis/actions/objects.md index 55751b13ff..dd8a9b648d 100644 --- a/docs/docs/apis/actions/objects.md +++ b/docs/docs/apis/actions/objects.md @@ -194,7 +194,7 @@ This object represents a list of user grant stored in ZITADEL. - `grants` Array of - `id` *string* - `projectGrantId` *string* - The id of the [project grant](../../concepts/usecases/saas#project-grant) + The id of the [project grant](/docs/guides/solution-scenarios/saas#project-grant) - `state` *Number* - `creationDate` *Date* diff --git a/docs/docs/guides/integrate/identity-providers/introduction.md b/docs/docs/concepts/features/identity-brokering.md similarity index 92% rename from docs/docs/guides/integrate/identity-providers/introduction.md rename to docs/docs/concepts/features/identity-brokering.md index aa004511ef..9b6c98f583 100644 --- a/docs/docs/guides/integrate/identity-providers/introduction.md +++ b/docs/docs/concepts/features/identity-brokering.md @@ -18,5 +18,5 @@ For example, if Google is configured as an identity provider in your organizatio Configure external identity providers on the instance level or just for one organization via the [Console](/guides/manage/console/instance-settings#identity-providers) or ZITADEL APIs. -The guides listed in this section will help you set up specific identity providers. +You will find [detailed integration guides for many Identity Providers](/guides/integrate/identity-providers) in our docs. ZITADEL also provides templates to configure generic identity providers, which don't have templates. diff --git a/docs/docs/guides/integrate/identity-providers/azure-ad.mdx b/docs/docs/guides/integrate/identity-providers/azure-ad.mdx index a681d58cf6..b87c08f1bf 100644 --- a/docs/docs/guides/integrate/identity-providers/azure-ad.mdx +++ b/docs/docs/guides/integrate/identity-providers/azure-ad.mdx @@ -90,11 +90,11 @@ In this case, configure "Accounts in any organizational directory and personal M - Organizations: Choose organization if you have Azure AD Tenants and no personal accounts. (You have configured either "Accounts in this organization" or "Accounts in any organizational directory" on your Azure APP) - Consumers: Choose this if you want to allow public accounts. (In your Azure AD App you have configured "Personal Microsoft accounts only") -**Tenant ID**: If you have selected either the *Organizations* or *Customers* as the *Tenant Type*, you have to enter the *Directory (Tenant) ID*, copied previously in the Azure App configuration, here. +**Tenant ID**: If you have selected *Tenant ID* as *Tenant Type*, you have to enter the *Directory (Tenant) ID* into the *Tenant ID* field, copied previously from the Azure App configuration. -![Azure Provider](/img/guides/zitadel_azure_provider.png) +![Azure Provider](/img/guides/zitadel_azure_provider2.png) ### Activate IdP diff --git a/docs/docs/guides/integrate/services/atlassian-saml.md b/docs/docs/guides/integrate/services/atlassian-saml.md index f4e1a81d0c..f6be4726f6 100644 --- a/docs/docs/guides/integrate/services/atlassian-saml.md +++ b/docs/docs/guides/integrate/services/atlassian-saml.md @@ -1,5 +1,6 @@ --- title: Connect with Atlassian through SAML 2.0 +sidebar_label: Atlassian --- This guide shows how to enable login with ZITADEL on Atlassian. diff --git a/docs/docs/guides/integrate/services/auth0-oidc.mdx b/docs/docs/guides/integrate/services/auth0-oidc.mdx index 6aa17008ba..628900434e 100644 --- a/docs/docs/guides/integrate/services/auth0-oidc.mdx +++ b/docs/docs/guides/integrate/services/auth0-oidc.mdx @@ -1,5 +1,6 @@ --- title: Connect with Auth0 through OIDC +sidebar_label: Auth0 (OIDC) --- import CreateApp from "../application/_application.mdx"; diff --git a/docs/docs/guides/integrate/services/auth0-saml.md b/docs/docs/guides/integrate/services/auth0-saml.md index 42ba1a1389..3742ccfa43 100644 --- a/docs/docs/guides/integrate/services/auth0-saml.md +++ b/docs/docs/guides/integrate/services/auth0-saml.md @@ -1,5 +1,6 @@ --- title: Connect with Auth0 through SAML 2.0 +sidebar_label: Auth0 (SAML) --- This guide shows how to enable login with ZITADEL on Auth0. diff --git a/docs/docs/guides/integrate/services/aws-saml.md b/docs/docs/guides/integrate/services/aws-saml.md index c5f762a0d2..26a5c12fcd 100644 --- a/docs/docs/guides/integrate/services/aws-saml.md +++ b/docs/docs/guides/integrate/services/aws-saml.md @@ -1,5 +1,6 @@ --- title: Connect with AWS through SAML 2.0 +sidebar_label: Amazon Web Services --- This guide shows how to enable login with ZITADEL on AWS SSO. diff --git a/docs/docs/guides/integrate/services/gitlab-saml.md b/docs/docs/guides/integrate/services/gitlab-saml.md index 5a15f0a228..fc9c51ca63 100644 --- a/docs/docs/guides/integrate/services/gitlab-saml.md +++ b/docs/docs/guides/integrate/services/gitlab-saml.md @@ -1,5 +1,6 @@ --- title: Connect with Gitlab through SAML 2.0 +sidebar_label: Gitlab --- This guide shows how to enable login with ZITADEL on Gitlab. diff --git a/docs/docs/guides/integrate/services/google-cloud.mdx b/docs/docs/guides/integrate/services/google-cloud.mdx new file mode 100644 index 0000000000..eb75bcf974 --- /dev/null +++ b/docs/docs/guides/integrate/services/google-cloud.mdx @@ -0,0 +1,132 @@ +--- +title: Google Cloud with Workforce Identity Federation (OIDC) +sidebar_label: Google Cloud +--- + +import CreateApp from "../application/_application.mdx"; + +This guide shows how to login users and assign roles with [Workforce Identity Federation to Google Cloud](https://cloud.google.com/iam/docs/workforce-identity-federation). + +It covers how to: + +- create and configure your application in ZITADEL +- configure an Action to transform claims +- create and configure the connection to Google Cloud with Workforce Identity Federation using OpenID Connect (OIDC) + +Prerequisites: + +- existing ZITADEL Instance, if not present follow [this guide](/guides/start/quickstart) +- existing ZITADEL Organization, if not present follow [this guide](/guides/manage/console/organizations) +- existing ZITADEL project, if not present follow the first 3 steps [here](/guides/manage/console/projects) +- prerequisites on Google Cloud side [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation). + +> We have to switch between ZITADEL and a Google Cloud. If the headings begin with "ZITADEL" switch to the ZITADEL Console and if +> the headings start with "Google Cloud" please refer to the configuration guide on Google Cloud. + +## **Google Cloud**: Configure + +Follow the steps **Before you begin**, **Required roles**, and **create a workforce identity pool** (OIDC) in the [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation). + +Before you create the workforce identity pool provider you should create your application in ZITADEL. + +## **ZITADEL**: Create the application + +In your existing project: + +First of all we create the application in your project. + +:::info +Google Cloud requires just an ID Token as JWT including the [described required and optional scopes](https://cloud.google.com/iam/docs/workforce-identity-federation#attribute-mappings). +::: + +Create a new application and click on "I'm a pro. Skip this wizard." + +- **Application Type**: Web +- **Grant Types**: Implicit +- **Response Type**: ID Token +- **Authentication Method**: None + +:::info +You need to add the redirect URL and configure token settings after creating the application. +::: + +![Create application screen](/img/guides/integrate/services/google-cloud-create-app.png) + +## **ZITADEL**: Redirect url + +![Redirect URL](/img/guides/integrate/services/google-cloud-redirect-url.png) + +After creating, go to the application settings "Redirect settings" and add the redirect url from Googles configuration guide. +It looks something like `https://auth.cloud.google/signin-callback/locations/global/workforcePools/WORKFORCE_POOL_ID/providers/WORKFORCE_PROVIDER_ID`. + +Save the settings. + +:::caution +Make sure to replace the `WORKFORCE_POOL_ID` and `WORKFORCE_PROVIDER_ID` with your values in the redirect url +::: + +## **ZITADEL**: Token settings + +![Token settings](/img/guides/integrate/services/google-cloud-token-settings.png) + +After creating, go to the application settings "Token settings" and configure as follows: + +- **Auth Token Type**: JWT +- **Add user roles to the access token**: disabled (optional) +- **User roles inside ID Token**: enabled +- **User Info inside ID Token**: enabled + +Save the settings. + +## **ZITADEL**: Custom claims + +Go to your project and create roles according to the Groups in Google Cloud. +Authorize a test user by assigning roles in ZITADEL. + +Google Cloud expects some claims, including groups, in a specific format as [described here](https://cloud.google.com/iam/docs/workforce-identity-federation#attribute-mappings). +Claims can be transformed in ZITADEL with [Actions](/apis/actions/introduction). + +Create an Action with the following code to flatten the roles and include the claim for the users' display name. + +:::info +If you want to configure a special attribute mapping in the workforce identity pool provider, then adjust the claims accordingly. +::: + +```javascript +function googleGroups(ctx, api) { + if (ctx.v1.user.grants == undefined || ctx.v1.user.grants.count == 0) { + return; + } + + let grants = []; + ctx.v1.user.grants.grants.forEach(claim => { + claim.roles.forEach(role => { + grants.push(claim.projectId+':'+role) + }) + }) + + api.v1.claims.setClaim('google.groups', grants) + api.v1.claims.setClaim('google.display_name', ctx.v1.getUser().human.displayName) +} + +``` + +![Action Code](/img/guides/integrate/services/google-cloud-action-code.png) + + + +:::caution +Make sure that the name of the action matches the name of the function. +::: + +And add the the Action Script to the following Flow and Trigger: + +- **Flow Type**: Complement Token +- **Trigger Type**: Pre access token creation +- **Actions**: googleGroups + +![Action Flow](/img/guides/integrate/services/google-cloud-action-flow.png) + +## **Google Cloud**: Create a WIP provider + +Complete the steps in the [in the configuration guide](https://cloud.google.com/iam/docs/configuring-workforce-identity-federation) with the `ISSUER_URI` and `CLIENT_ID` from ZITADEL. diff --git a/docs/docs/guides/integrate/services/pingidentity-saml.md b/docs/docs/guides/integrate/services/pingidentity-saml.md index fe5b88859c..ce827ef1b4 100644 --- a/docs/docs/guides/integrate/services/pingidentity-saml.md +++ b/docs/docs/guides/integrate/services/pingidentity-saml.md @@ -1,5 +1,6 @@ --- title: Connect with Ping Identity through SAML 2.0 +sidebar_label: Ping Identity --- This guide shows how to enable login with ZITADEL on Auth0. diff --git a/docs/docs/guides/manage/console/instance-settings.mdx b/docs/docs/guides/manage/console/instance-settings.mdx index 1472365102..584d095a5f 100644 --- a/docs/docs/guides/manage/console/instance-settings.mdx +++ b/docs/docs/guides/manage/console/instance-settings.mdx @@ -141,8 +141,7 @@ Configure the different lifetimes checks for the login process: You can configure all kinds of external identity providers for identity brokering, which support OIDC (OpenID Connect). Create a new identity provider configuration and enable it in the list afterwards. -For a detailed guide about how to configure a new identity provider for identity brokering have a look at our guide: -[Identity Brokering](/guides/integrate/identity-providers/introduction.md) +For a detailed guide about how to configure a new identity provider for identity brokering have a look at our [identity provider guides](/guides/integrate/identity-providers). ## Password Complexity @@ -176,13 +175,22 @@ If an account is locked, the administrator has to unlock it in the ZITADEL conso ## Domain settings -In the domain policy you have two different settings. -One is the "user_login_must_be_domain", by setting this all the users within an organisation will be suffixed with the domain of the organisation. +### Add organization domain as suffix to loginnames + +If you enable this setting, all loginnames will be suffixed with the organization domain. If this settings is disabled, you have to ensure that usernames are unique over all organizations. + +### Validate Org domains + +If this is enabled all created domains on an organization must be verified per dns/acme challenge. -The second is "validate_org_domains" if this is set to true all created domains on an organisation must be verified per acme challenge. More about how to verify a domain [here](/guides/manage/console/organizations#domain-verification-and-primary-domain). If it is set to false, all registered domain will automatically be created as verified and the users will be able to use the domain for login. +### SMTP Sender Address matches Instance Domain + +If enabled, the SMTP server address must match the instance's primary domain. +With that you can ensure that users receive notifications from the same domain that is used for login. + ### Use email as username To be able to use the email as username you have to disable the attribute "User Loginname must contain orgdomain" on your domain settings. @@ -191,6 +199,8 @@ All usernames will then be globally unique within your instance. You can either set this attribute on your whole ZITADEL instance or just on some specific organizations. +Please refer to the [configuration guide](/docs/guides/solution-scenarios/configurations#use-email-to-login) for more information. + ## Privacy Policy and TOS With this setting you are able to configure your privacy policy, terms of service, help links and help/support email address. diff --git a/docs/docs/guides/manage/console/organizations.mdx b/docs/docs/guides/manage/console/organizations.mdx index 32b6ee4c45..b359c3ca2e 100644 --- a/docs/docs/guides/manage/console/organizations.mdx +++ b/docs/docs/guides/manage/console/organizations.mdx @@ -67,6 +67,10 @@ Please note that domain verification also removes the logonname from all users, ## Verify your domain name +:::info +You can also disable domain verification with DNS challenge in the [instance settings](/docs/guides/manage/console/instance-settings#domain-settings). +::: + 1. Browse to your organization 2. Click **Add Domain** 3. To start the domain verification click the domain name and a dialog will appear, where you can choose between DNS or HTTP challenge methods. @@ -75,7 +79,9 @@ Please note that domain verification also removes the logonname from all users, ![Verify Domain](/img/console_verify_domain.gif) -> **_Please note:_** Do not delete the verification code, as ZITADEL will re-check the ownership of your domain from time to time +:::caution +Do not delete the verification code, as ZITADEL will re-check the ownership of your domain from time to time +::: ## Organization Settings @@ -97,3 +103,12 @@ Those settings are the same as on your instance. If you need custom branding on a organization (for example in a B2B scenario, where organizations are allowed to use their custom design), navigate back to the home page, choose your organization in the header above, navigate to the organization settings and set the custom design here. The behaviour of the login page, applyling custom design, is then defined on your projects detail page. Read more about it [here](./projects#branding) + +## Default organization + +On the instance settings page ($YOUR_DOMAIN//ui/console/orgs) you can set an organization as default organization. +Click the "..." on the right hand side of the table and select "Set as default organization". + +The current default organization is marked by a label "Default". + +When no organization was selected (eg, with the auth request or through [Domain Discovery](/docs/guides/solution-scenarios/domain-discovery)), then all users are allowed to login and users can self-register to this default organization. diff --git a/docs/docs/guides/manage/customize/behavior.md b/docs/docs/guides/manage/customize/behavior.md index 061ac9ef06..fc45f9305d 100644 --- a/docs/docs/guides/manage/customize/behavior.md +++ b/docs/docs/guides/manage/customize/behavior.md @@ -11,7 +11,7 @@ Before you start, make sure you have everything set up correctly. - You need to be at least a ZITADEL _ORG_OWNER_ - Your ZITADEL organization needs to have the actions feature enabled. -- [Your ZITADEL organization needs to have at least one external identity provider enabled](../../integrate/identity-providers/introduction.md) +- [Your ZITADEL organization needs to have at least one external identity provider enabled](../../integrate/identity-providers) - [You need to have at least one role configured for a project](../console/projects) ## Copy some information for the action diff --git a/docs/docs/guides/migrate/sources/auth0.md b/docs/docs/guides/migrate/sources/auth0.md index dca95a7c3a..977385c1c1 100644 --- a/docs/docs/guides/migrate/sources/auth0.md +++ b/docs/docs/guides/migrate/sources/auth0.md @@ -7,6 +7,7 @@ Migrating users from Auth0 to ZITADEL requires the following steps: - Request and download hashed passwords - Export all user data +- Run migration tool to merge Auth0 users and passwords - Import users and password hashes to ZITADEL ## Export hashed passwords @@ -34,8 +35,37 @@ See this [community post](https://community.auth0.com/t/password-hashes-export-d Create a [bulk user export](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports) from the Auth0 Management API. You will receive a newline-delimited JSON with the requested user data. +This is an example request, we have included the user id, the email and the name of the user. Make sure to export the users in a json format. + +```bash +curl --request POST \ + --url $AUTH0_DOMAIN/api/v2/jobs/users-exports \ + --header 'authorization: Bearer $TOKEN' \ + --header 'content-type: application/json' \ + --data '{ + "connection_id": "$CONNECTION_ID", + "format": "json", + "fields": [ + {"name": "user_id"}, + {"name": "email"}, + {"name": "name"}, + ] +}' +``` + +## Run Migration Tool + +We have developed a tool that combines your exported user data with their corresponding passwords to generate the import request body for ZITADEL. + +1. Download the latest release of [github.com/zitadel/zitadel-tools](https://github.com/zitadel/zitadel-tools/releases) +2. Execute the binary with the following flags: + ```bash + ./zitadel-tools migrate auth0 --org= --users=./users.json --passwords=./passwords.json --output=./importBody.json + ``` + Use the Organization ID from your ZITADEL instance where you like to add the users. +3. You will now get a new file importBody.json which contains the body for the request to the import of ZITADEL + ## Import users and password hashes to ZITADEL -You will need to merge the received password hashes with the user bulk export. - -After you successfully merged the datasets, you can follow the instructions described in the [Migrate Users](../users) guide to import users to ZITADEL. +Copy the content from the importBody.json file created in the last step. +You can now follow the instructions described in the [Migrate Users](../users) guide to import users to ZITADEL. diff --git a/docs/docs/guides/solution-scenarios/configurations.mdx b/docs/docs/guides/solution-scenarios/configurations.mdx index f5a11b445f..d7d047d288 100644 --- a/docs/docs/guides/solution-scenarios/configurations.mdx +++ b/docs/docs/guides/solution-scenarios/configurations.mdx @@ -1,6 +1,6 @@ --- title: How to configure ZITADEL for your scenario -sidebar_label: FAQ Configurations +sidebar_label: Common configurations --- Each customer does have different needs and use-cases. In ZITADEL you are able to configure your settings depending on your needs. @@ -15,7 +15,7 @@ If a user of this organization wants to login, you don't want them to enter thei ### Settings 1. Go to the "Identity Providers" Settings of the organization -2. Configure the needed identity provider: Read this [guide](../integrate/identity-providers/introduction.md) if you don't know how +2. Configure the needed identity provider: Read this [guide](../integrate/identity-providers) if you don't know how 3. Go to the "Login Behavior and Security" settings of the organization 4. Disable "Username Password Allowed" and enable "External IDP allowed" in the Advanced Section @@ -27,9 +27,9 @@ More about the [scopes](/apis/openidoauth/scopes#reserved-scopes) If you have an application that runs a dedicated domain for each customer you need to instruct ZITADEL to allow redirection for each domain specifically to safeguard against phishing attacks. -Example: -MyApplication: customer-a.app.com -ZITADEL Login: login.app.com +Example: +MyApplication: `customer-a.app.com` +ZITADEL Login: `login.app.com` In the OIDC Authorization request you always have to send the redirect URI to where you like to be redirected after login. To handle this scenario it is possible to register multiple URIs on each application in ZITADEL, the only criteria is that the requested URI has to match one of the registered URIs. diff --git a/docs/docs/guides/solution-scenarios/domain-discovery.mdx b/docs/docs/guides/solution-scenarios/domain-discovery.mdx new file mode 100644 index 0000000000..25bf28c9a8 --- /dev/null +++ b/docs/docs/guides/solution-scenarios/domain-discovery.mdx @@ -0,0 +1,127 @@ +--- +title: Domain Discovery +--- + +This guide should explain how domain discovery works and how to configure it in ZITADEL. + +## Overview + +Domain discovery is typically used in [B2B](./b2b) or [SaaS](./saas) scenarios where you have users from different organizations and you want to route them according to their login methods, which could be a user name or, depending on your configuration, also an [email / phone number](configurations#use-email-to-login). + +![Overview Domain Discovery](/img/guides/solution-scenarios/domain-discovery.png) + +In the example there is a service provider with a ZITADEL instance running on a [custom domain](/docs/guides/manage/cloud/instances#add-custom-domain) on `login.mycompany.com`. +By default all users login on the organization **CIAM** with their preferred social login provider. + +Users of the two business customers **Alpha** and **Beta** should login according to their organization login and access policy settings. +In case of Alpha users will login via an external identity provider (eg, [AzureAD](/docs/guides/integrate/identity-providers/azure-ad)). +Beta users must only login with username/password and MFA instead. + +For this scenario you need to route the user `alice@alpha.com` to the **Alpha Organization** and `bob@beta.com` to the **Beta Organization** respectively. + +Follow this guide to configure your ZITADEL instance for this scenario. + +## Instance + +### Default Login Page + +You will use the instance default settings for the login for the organization **CIAM**. +When opening `login.mycompany.com` then the login policy of the instance will be applied. +This means that you have to configure the [Login and Access](/docs/guides/manage/console/instance-settings#login-behaviour-and-access) Policy and [Identity Providers](/docs/guides/manage/console/instance-settings#identity-providers) for the **CIAM** users on the instance itself. + +:::info +You can also configure these settings on the default organization (see below) and send the scope `urn:zitadel:iam:org:id:{id}` with every [auth request](/docs/apis/openidoauth/authrequest#organization-policies-and-branding). +::: + +### Default Organization + +Set **CIAM** as [default organization](/docs/guides/manage/console/organizations#default-organization). +You will find the overview of all organizations under the "Organizations" tab on the Instance Settings. + +The default organization will hold all unmatched users, ie. all users that are not specifically in the organizations **Alpha** or **Beta** in the example. + +### Enable Domain Discovery + +In the [Login Behavior and Security Settings](/docs/guides/manage/console/instance-settings#login-behaviour-and-access) enable "Domain discovery allowed" + +### Configure login with email + +Follow this [configuration guide](/docs/guides/solution-scenarios/configurations#use-email-to-login) to allow users to login with their email address. + +### Other considerations + +You can also have multiple custom domains pointing to the same instance as described in this [configuration guide](/docs/guides/solution-scenarios/configurations#custom-application-domain-per-organization). In our example you could also use `alpha.mycompany.com` to show the login page of your instance. + +The domain of your email notification can be changed by [setting up your SMTP](/docs/guides/manage/console/instance-settings#smtp). + +## Organization + +### Alpha organization + +Users of **Alpha** should only be allowed to authenticate with their company's identity provider. + +In the organization settings under Login Behavior and Access make sure the following settings are applied: + +- **Username Password allowed**: Disabled +- **Register allowed**: Disabled - we will configure this on the external identity provider +- **External IDP allowed**: Enabled + +Now you can configure an [external identity provider](/docs/guides/manage/console/instance-settings#identity-providers). + +:::info +Given you have only one external identity provider configured, when a user tries to login on that organization, then the user will be automatically redirected to the external identity provider. +In case multiple providers are configured, then the user will be prompted to select an identity provider. +::: + +### Beta organization + +Users of **Beta** must create an account and login with password and 2FA. + +In the organization settings under Login Behavior and Access make sure the following settings are applied: + +- **Username Password allowed**: Enabled +- **Register allowed**: Disabled - you may want [Managers](/docs/concepts/structure/managers) to setup accounts. +- **External IDP allowed**: Disabled + +Make sure to [Force MFA](/docs/guides/manage/console/instance-settings#multifactor-mfa) so that users must setup a second factor for authentication. + +### Verify domains + +Switch to the organization **Alpha** and select the tab "Domains". +Verify the domain alpha.com following the [organization guide](/docs/guides/manage/console/organizations#domain-verification-and-primary-domain). + +Do the same for the **Beta** organization. + +:::info +You can also disable domain verification with acme challenge in the [instance settings](/docs/guides/manage/console/instance-settings#domain-settings). +::: + +## Conclusion + +You should be all setup to try out domain discovery. + +The user journeys for the different users would look as follows: + +- User (Alice, Bob, Chuck) clicks a login button in your application +- Redirected to `login.mycompany.com` (ZITADEL running under a custom domain) + +Chuck + +1. Select Google button +1. Redirect to Google IDP +1. Chuck logs in with Google credentials +1. Redirected back to your application + +Alice + +1. Alice enters alice@alpha.com and clicks next +1. Redirect to AzureAD Tenant (or any other IDP) +1. Alice logs in with her company credentials +1. Redirected back to your application + +Bob + +1. Bob enters bob@beta.com and clicks next +1. Bob will be redirected to a login with the branding of beta.com +1. Bob enters his password and MFA on the login screen +1. Redirected back to your application diff --git a/docs/docs/guides/solution-scenarios/introduction.mdx b/docs/docs/guides/solution-scenarios/introduction.mdx deleted file mode 100644 index bf375a8ea3..0000000000 --- a/docs/docs/guides/solution-scenarios/introduction.mdx +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Introduction ---- - -import { - ListElement, - ListWrapper, - ICONTYPE, -} from "../../../src/components/list"; -import Column from "../../../src/components/column"; - -## Solution Scenarios - -Customers of an SaaS Identity and Access Management System usually have all distinct use cases and requirements. -This guide attempts to explain real-world implementations and break them down into **Solution Scenarios** which aim to help you getting started with ZITADEL. - - - - - diff --git a/docs/docs/concepts/usecases/saas.md b/docs/docs/guides/solution-scenarios/saas.md similarity index 100% rename from docs/docs/concepts/usecases/saas.md rename to docs/docs/guides/solution-scenarios/saas.md diff --git a/docs/docs/legal/rate-limit-policy.md b/docs/docs/legal/rate-limit-policy.md index d3e3047a4a..8cae76e99a 100644 --- a/docs/docs/legal/rate-limit-policy.md +++ b/docs/docs/legal/rate-limit-policy.md @@ -12,18 +12,32 @@ To ensure the availability of our Services and to avoid slow or failed requests ## How is the rate limit implemented -ZITADEL Clouds rate limit is built around a `IP` oriented model. Please be aware that we also utilize a service for DDoS mitigation. +ZITADEL Clouds rate limit is built around a `IP` oriented model. +Please be aware that we also utilize a service for DDoS mitigation. So if you simply change your `IP` address and run the same request again and again you might be get blocked at some point. If you are blocked you will receive a `http status 429`. -:::tip +:::tip Implement exponential backoff You should consider to implement [exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) into your application to prevent a blocking loop. ::: +:::info Raising limits +We understand that there are certain scenarios where your users access ZITADEL from shared IP Addresses. +For example if you use a corporate proxy or Network Address Translation NAT. +Please [get in touch](https://zitadel.com/contact) with us to discuss your requirements and we'll find a solution. +::: + ## What rate limits do apply -For ZITADEL Cloud, we have a rate limiting rule for login paths (login, register and reset features) and for API paths each. Learn more about [the exact rules](/apis/ratelimits). +For ZITADEL Cloud, we have a rate limiting rule for login paths (login, register and reset features) and for API paths each. + +Rate limits are implemented with the following rules: + +| Path | Description | Rate Limiting | One Minute Banning | +|--------------------------|----------------------------------------|--------------------------------------|----------------------------------------| +| /ui/login* | Global Login, Register and Reset Limit | 10 requests per second over a minute | 15 requests per second over 3 minutes | +| All other paths | All gRPC- and REST APIs as well as the ZITADEL Customer Portal | 10 requests per second over a minute | 10 requests per second over 3 minutes | ## Load Testing diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 43cfb96a2c..a660dae88d 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -259,6 +259,13 @@ module.exports = { sidebarOptions: { groupPathsBy: "tag", }, + }, + user: { + specPath: ".artifacts/openapi/zitadel/user/v2alpha/user_service.swagger.json", + outputDir: "docs/apis/user_service", + sidebarOptions: { + groupPathsBy: "tag", + }, } } }, diff --git a/docs/sidebars.js b/docs/sidebars.js index b967f0183f..2d7bc5578d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -126,8 +126,14 @@ module.exports = { { type: "category", label: "Integrate", + link: { + type: "generated-index", + title: "Integrate", + slug: "guides/integrate", + description: + "Integrate your users and application with ZITADEL. In this section you will find resource on how to authenticate your users, configure external identity providers, access the ZITADEL APIs to manage resources, and integrate with third party services and tools.", + }, items: [ - { type: "category", label: "Authenticate Users", @@ -141,9 +147,16 @@ module.exports = { { type: "category", label: "Configure Identity Providers", + link: { + type: "generated-index", + title: "Let users login with their preferred identity provider", + slug: "/guides/integrate/identity-providers", + description: + "In the following guides you will learn how to configure and setup your preferred external identity provider in ZITADEL.", + + }, collapsed: true, items: [ - "guides/integrate/identity-providers/introduction", "guides/integrate/identity-providers/google", "guides/integrate/identity-providers/azure-ad", "guides/integrate/identity-providers/github", @@ -186,10 +199,19 @@ module.exports = { { type: "category", label: "Services", + link: { + type: "generated-index", + title: "Integrate ZITADEL with your favorite services", + slug: "/guides/integrate/services", + description: + "With the guides in this section you will learn how to integrate ZITADEL with your services.", + + }, collapsed: true, items: [ "guides/integrate/services/gitlab-self-hosted", "guides/integrate/services/aws-saml", + "guides/integrate/services/google-cloud", "guides/integrate/services/atlassian-saml", "guides/integrate/services/gitlab-saml", "guides/integrate/services/auth0-oidc", @@ -200,6 +222,14 @@ module.exports = { { type: "category", label: "Tools", + link: { + type: "generated-index", + title: "Integrate ZITADEL with your tools", + slug: "/guides/integrate/tools", + description: + "With the guides in this section you will learn how to integrate ZITADEL with your favorite tools.", + + }, collapsed: true, items: [ "guides/integrate/authenticated-mongodb-charts", @@ -211,12 +241,19 @@ module.exports = { { type: "category", label: "Solution Scenarios", + link: { + type: "generated-index", + title: "Solution Scenarios", + slug: "guides/solution-scenarios/introduction", + description: + "Customers of an SaaS Identity and Access Management System usually have all distinct use cases and requirements. This guide attempts to explain real-world implementations and break them down into Solution Scenarios which aim to help you getting started with ZITADEL.", + }, collapsed: true, items: [ - "guides/solution-scenarios/introduction", "guides/solution-scenarios/b2c", "guides/solution-scenarios/b2b", - "concepts/usecases/saas", + "guides/solution-scenarios/saas", + "guides/solution-scenarios/domain-discovery", "guides/solution-scenarios/configurations", ], }, @@ -234,6 +271,7 @@ module.exports = { "concepts/structure/users", "concepts/structure/managers", "concepts/structure/policies", + "concepts/features/identity-brokering", "concepts/structure/jwt_idp", "concepts/features/actions", "concepts/features/selfservice", @@ -337,6 +375,20 @@ module.exports = { }, items: require("./docs/apis/system/sidebar.js"), }, + { + type: "category", + label: "User Lifecycle (Alpha)", + link: { + type: "generated-index", + title: "User Service API (Alpha)", + slug: "/apis/user_service", + description: + "This API is intended to manage users in a ZITADEL instance.\n"+ + "\n"+ + "This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login.", + }, + items: require("./docs/apis/user_service/sidebar.js"), + }, { type: "category", label: "Assets", @@ -396,10 +448,9 @@ module.exports = { items: ["apis/observability/metrics", "apis/observability/health"], }, { - type: "category", - label: "Rate Limits", - collapsed: false, - items: ["apis/ratelimits/ratelimits", "legal/rate-limit-policy"], + type: 'link', + label: 'Rate Limits (Cloud)', // The link label + href: '/legal/rate-limit-policy', // The internal path }, ], selfHosting: [ diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 13dd57f7fc..f98fad11b6 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -291,6 +291,7 @@ h2 { --ifm-color-warning-dark: #4f566b; --ifm-toc-border-color: rgba(135, 149, 161, 0.2); --ifm-table-border-color: rgba(135, 149, 161, 0.2); + --ifm-card-background-color: #1a253c; --card-background: #1a253c; /* #1a1d46; */ --list-background: #1a253c; /* #1a1d46; */ --apiauthbackground: linear-gradient(40deg, #506e6e90 30%, #506e6e90); diff --git a/docs/static/img/guides/integrate/services/google-cloud-action-code.png b/docs/static/img/guides/integrate/services/google-cloud-action-code.png new file mode 100644 index 0000000000..f88d743f1a Binary files /dev/null and b/docs/static/img/guides/integrate/services/google-cloud-action-code.png differ diff --git a/docs/static/img/guides/integrate/services/google-cloud-action-flow.png b/docs/static/img/guides/integrate/services/google-cloud-action-flow.png new file mode 100644 index 0000000000..5abac95f9c Binary files /dev/null and b/docs/static/img/guides/integrate/services/google-cloud-action-flow.png differ diff --git a/docs/static/img/guides/integrate/services/google-cloud-create-app.png b/docs/static/img/guides/integrate/services/google-cloud-create-app.png new file mode 100644 index 0000000000..35ed509b36 Binary files /dev/null and b/docs/static/img/guides/integrate/services/google-cloud-create-app.png differ diff --git a/docs/static/img/guides/integrate/services/google-cloud-redirect-url.png b/docs/static/img/guides/integrate/services/google-cloud-redirect-url.png new file mode 100644 index 0000000000..74d62f21ee Binary files /dev/null and b/docs/static/img/guides/integrate/services/google-cloud-redirect-url.png differ diff --git a/docs/static/img/guides/integrate/services/google-cloud-token-settings.png b/docs/static/img/guides/integrate/services/google-cloud-token-settings.png new file mode 100644 index 0000000000..01fe3ef594 Binary files /dev/null and b/docs/static/img/guides/integrate/services/google-cloud-token-settings.png differ diff --git a/docs/static/img/guides/solution-scenarios/domain-discovery.png b/docs/static/img/guides/solution-scenarios/domain-discovery.png new file mode 100644 index 0000000000..b116c0aabe Binary files /dev/null and b/docs/static/img/guides/solution-scenarios/domain-discovery.png differ diff --git a/docs/static/img/guides/zitadel_azure_provider.png b/docs/static/img/guides/zitadel_azure_provider.png deleted file mode 100644 index 371148b6a7..0000000000 Binary files a/docs/static/img/guides/zitadel_azure_provider.png and /dev/null differ diff --git a/docs/static/img/guides/zitadel_azure_provider2.png b/docs/static/img/guides/zitadel_azure_provider2.png new file mode 100644 index 0000000000..9007695058 Binary files /dev/null and b/docs/static/img/guides/zitadel_azure_provider2.png differ diff --git a/go.mod b/go.mod index eb61ce26e2..668aa02305 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/minio/minio-go/v7 v7.0.50 github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/gamut v0.3.1 + github.com/muhlemmer/gu v0.3.1 github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/pkg/errors v0.9.1 github.com/pquerna/otp v1.4.0 @@ -57,7 +58,7 @@ require ( github.com/superseriousbusiness/exifremove v0.0.0-20210330092427-6acd27eac203 github.com/ttacon/libphonenumber v1.2.1 github.com/zitadel/logging v0.3.4 - github.com/zitadel/oidc/v2 v2.2.6 + github.com/zitadel/oidc/v2 v2.4.0 github.com/zitadel/saml v0.0.11 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.40.0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 @@ -70,10 +71,10 @@ require ( go.opentelemetry.io/otel/sdk/metric v0.37.0 go.opentelemetry.io/otel/trace v1.14.0 golang.org/x/crypto v0.7.0 - golang.org/x/net v0.8.0 - golang.org/x/oauth2 v0.6.0 + golang.org/x/net v0.9.0 + golang.org/x/oauth2 v0.7.0 golang.org/x/sync v0.1.0 - golang.org/x/text v0.8.0 + golang.org/x/text v0.9.0 golang.org/x/tools v0.7.0 google.golang.org/api v0.115.0 google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd @@ -90,7 +91,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/google/pprof v0.0.0-20230323073829-e72429f035bd // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/muhlemmer/gu v0.3.1 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.14.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 4efcc17bb2..382d482406 100644 --- a/go.sum +++ b/go.sum @@ -1130,8 +1130,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zitadel/logging v0.3.4 h1:9hZsTjMMTE3X2LUi0xcF9Q9EdLo+FAezeu52ireBbHM= github.com/zitadel/logging v0.3.4/go.mod h1:aPpLQhE+v6ocNK0TWrBrd363hZ95KcI17Q1ixAQwZF0= -github.com/zitadel/oidc/v2 v2.2.6 h1:L2k5q1X8Rucax5Ynp3B3lz7JQDJxUwfWCOmgc9Bh0BM= -github.com/zitadel/oidc/v2 v2.2.6/go.mod h1:tGkj9lQk6KVj5hsM89XPadvi6I06666sMy3KtykvSFM= +github.com/zitadel/oidc/v2 v2.4.0 h1:BKx61qOxDf+GjrY8T6lFxPjea0aMfkFvHD9pqyJGpFk= +github.com/zitadel/oidc/v2 v2.4.0/go.mod h1:wBOrfB0m/tGXo6isym1F5k3VeXSUinGsAt2H8V/+Uks= github.com/zitadel/saml v0.0.11 h1:kObucnBrcu1PHCO7RGT0iVeuJL/5I50gUgr40S41nMs= github.com/zitadel/saml v0.0.11/go.mod h1:YGWAvPZRv4DbEZ78Ht/2P0AWzGn+6WGhFf90PMXl0Po= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= @@ -1342,8 +1342,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1360,8 +1360,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.6.0 h1:Lh8GPgSKBfWSwFvtuWOfeI3aAAnbXTSutYxJiOJFgIw= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1477,8 +1477,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/admin/repository/eventsourcing/handler/styling.go b/internal/admin/repository/eventsourcing/handler/styling.go index 53a7f89812..8d5107e788 100644 --- a/internal/admin/repository/eventsourcing/handler/styling.go +++ b/internal/admin/repository/eventsourcing/handler/styling.go @@ -65,16 +65,16 @@ func (_ *Styling) AggregateTypes() []models.AggregateType { return []models.AggregateType{org.AggregateType, instance.AggregateType} } -func (m *Styling) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := m.view.GetLatestStylingSequence(instanceID) +func (m *Styling) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { + sequence, err := m.view.GetLatestStylingSequence(ctx, instanceID) if err != nil { return 0, err } return sequence.CurrentSequence, nil } -func (m *Styling) EventQuery(instanceIDs []string) (*models.SearchQuery, error) { - sequences, err := m.view.GetLatestStylingSequences(instanceIDs) +func (m *Styling) EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) { + sequences, err := m.view.GetLatestStylingSequences(ctx, instanceIDs) if err != nil { return nil, err } diff --git a/internal/admin/repository/eventsourcing/repository.go b/internal/admin/repository/eventsourcing/repository.go index d7c03c1fc9..febc2629de 100644 --- a/internal/admin/repository/eventsourcing/repository.go +++ b/internal/admin/repository/eventsourcing/repository.go @@ -23,8 +23,8 @@ type EsRepository struct { eventstore.AdministratorRepo } -func Start(ctx context.Context, conf Config, static static.Storage, dbClient *database.DB, esV2 *eventstore2.Eventstore) (*EsRepository, error) { - es, err := v1.Start(dbClient) +func Start(ctx context.Context, conf Config, static static.Storage, dbClient *database.DB, esV2 *eventstore2.Eventstore, allowOrderByCreationDate bool) (*EsRepository, error) { + es, err := v1.Start(dbClient, allowOrderByCreationDate) if err != nil { return nil, err } diff --git a/internal/admin/repository/eventsourcing/view/sequence.go b/internal/admin/repository/eventsourcing/view/sequence.go index 4985fbef48..8de577f89e 100644 --- a/internal/admin/repository/eventsourcing/view/sequence.go +++ b/internal/admin/repository/eventsourcing/view/sequence.go @@ -1,6 +1,7 @@ package view import ( + "context" "time" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -15,12 +16,12 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error { return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate) } -func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) { - return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID) +func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) { + return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID) } -func (v *View) latestSequences(viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) { - return repository.LatestSequences(v.Db, sequencesTable, viewName, instanceIDs) +func (v *View) latestSequences(ctx context.Context, viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return repository.LatestSequences(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceIDs) } func (v *View) AllCurrentSequences(db, instanceID string) ([]*repository.CurrentSequence, error) { diff --git a/internal/admin/repository/eventsourcing/view/styling.go b/internal/admin/repository/eventsourcing/view/styling.go index 25ac4c882a..ac477b5d96 100644 --- a/internal/admin/repository/eventsourcing/view/styling.go +++ b/internal/admin/repository/eventsourcing/view/styling.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/iam/repository/view" "github.com/zitadel/zitadel/internal/iam/repository/view/model" @@ -39,12 +41,12 @@ func (v *View) UpdateOrgOwnerRemovedStyling(event *models.Event) error { return v.ProcessedStylingSequence(event) } -func (v *View) GetLatestStylingSequence(instanceID string) (*global_view.CurrentSequence, error) { - return v.latestSequence(stylingTyble, instanceID) +func (v *View) GetLatestStylingSequence(ctx context.Context, instanceID string) (*global_view.CurrentSequence, error) { + return v.latestSequence(ctx, stylingTyble, instanceID) } -func (v *View) GetLatestStylingSequences(instanceIDs []string) ([]*global_view.CurrentSequence, error) { - return v.latestSequences(stylingTyble, instanceIDs) +func (v *View) GetLatestStylingSequences(ctx context.Context, instanceIDs []string) ([]*global_view.CurrentSequence, error) { + return v.latestSequences(ctx, stylingTyble, instanceIDs) } func (v *View) ProcessedStylingSequence(event *models.Event) error { diff --git a/internal/admin/repository/eventsourcing/view/view.go b/internal/admin/repository/eventsourcing/view/view.go index 8f27706985..095e7c1dfa 100644 --- a/internal/admin/repository/eventsourcing/view/view.go +++ b/internal/admin/repository/eventsourcing/view/view.go @@ -1,12 +1,17 @@ package view import ( + "context" + "github.com/jinzhu/gorm" + + "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" ) type View struct { - Db *gorm.DB + Db *gorm.DB + client *database.DB } func StartView(sqlClient *database.DB) (*View, error) { @@ -15,10 +20,15 @@ func StartView(sqlClient *database.DB) (*View, error) { return nil, err } return &View{ - Db: gorm, + Db: gorm, + client: sqlClient, }, nil } func (v *View) Health() (err error) { return v.Db.DB().Ping() } + +func (v *View) TimeTravel(ctx context.Context, tableName string) string { + return tableName + v.client.Timetravel(call.Took(ctx)) +} diff --git a/internal/api/api.go b/internal/api/api.go index 3cb851e3d0..d634e50109 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -101,6 +101,12 @@ func (a *API) RegisterService(ctx context.Context, grpcServer server.Server) err return nil } +// HandleFunc allows registering a [http.HandlerFunc] on an exact +// path, instead of prefix like RegisterHandlerOnPrefix. +func (a *API) HandleFunc(path string, f http.HandlerFunc) { + a.router.HandleFunc(path, f) +} + // RegisterHandlerOnPrefix registers a http handler on a path prefix // the prefix will not be passed to the actual handler func (a *API) RegisterHandlerOnPrefix(prefix string, handler http.Handler) { diff --git a/internal/api/authz/authorization.go b/internal/api/authz/authorization.go index c3c6d55d4b..0df781f4b0 100644 --- a/internal/api/authz/authorization.go +++ b/internal/api/authz/authorization.go @@ -14,11 +14,11 @@ const ( authenticated = "authenticated" ) -func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { +func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) { ctx, span := tracing.NewServerInterceptorSpan(ctx) defer func() { span.EndWithError(err) }() - ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, verifier, method) + ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method) if err != nil { return nil, err } @@ -29,7 +29,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s }, nil } - requestedPermissions, allPermissions, err := getUserMethodPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig, ctxData) + requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID) if err != nil { return nil, err } @@ -110,18 +110,6 @@ func HasGlobalPermission(perms []string) bool { return false } -func HasGlobalExplicitPermission(perms []string, permToCheck string) bool { - for _, perm := range perms { - p, ctxID := SplitPermission(perm) - if p == permToCheck { - if ctxID == "" { - return true - } - } - } - return false -} - func GetAllPermissionCtxIDs(perms []string) []string { ctxIDs := make([]string, 0) for _, perm := range perms { @@ -132,16 +120,3 @@ func GetAllPermissionCtxIDs(perms []string) []string { } return ctxIDs } - -func GetExplicitPermissionCtxIDs(perms []string, searchPerm string) []string { - ctxIDs := make([]string, 0) - for _, perm := range perms { - p, ctxID := SplitPermission(perm) - if p == searchPerm { - if ctxID != "" { - ctxIDs = append(ctxIDs, ctxID) - } - } - } - return ctxIDs -} diff --git a/internal/api/authz/config.go b/internal/api/authz/config.go index d9b79ee19c..865349d953 100644 --- a/internal/api/authz/config.go +++ b/internal/api/authz/config.go @@ -14,11 +14,11 @@ type MethodMapping map[string]Option type Option struct { Permission string CheckParam string - Feature string + AllowSelf bool } -func (a *Config) getPermissionsFromRole(role string) []string { - for _, roleMap := range a.RolePermissionMappings { +func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string { + for _, roleMap := range rolePermissionMappings { if roleMap.Role == role { return roleMap.Permissions } diff --git a/internal/api/authz/permissions.go b/internal/api/authz/permissions.go index 74ffa80c79..b392ca7958 100644 --- a/internal/api/authz/permissions.go +++ b/internal/api/authz/permissions.go @@ -7,7 +7,28 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" ) -func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPerm string, authConfig Config, ctxData CtxData) (requestedPermissions, allPermissions []string, err error) { +func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) { + ctxData := GetCtxData(ctx) + if allowSelf && ctxData.UserID == resourceID { + return nil + } + requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID) + if err != nil { + return err + } + + _, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions") + err = checkUserResourcePermissions(requestedPermissions, resourceID) + userPermissionSpan.EndWithError(err) + if err != nil { + return err + } + return nil +} + +// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level), +// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately. +func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() @@ -16,13 +37,13 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer } ctx = context.WithValue(ctx, dataKey, ctxData) - memberships, err := t.SearchMyMemberships(ctx) + memberships, err := resolver.SearchMyMemberships(ctx, orgID) if err != nil { return nil, nil, err } if len(memberships) == 0 { err = retry(func() error { - memberships, err = t.SearchMyMemberships(ctx) + memberships, err = resolver.SearchMyMemberships(ctx, orgID) if err != nil { return err } @@ -35,24 +56,56 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer return nil, nil, nil } } - requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, authConfig) + requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings) return requestedPermissions, allPermissions, nil } -func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, authConfig Config) (requestPermissions, allPermissions []string) { +// checkUserResourcePermissions checks that if a user i granted either the requested permission globally (project.write) +// or the specific resource (project.write:123) +func checkUserResourcePermissions(userPerms []string, resourceID string) error { + if len(userPerms) == 0 { + return errors.ThrowPermissionDenied(nil, "AUTH-AWfge", "No matching permissions found") + } + + if resourceID == "" { + return nil + } + + if HasGlobalPermission(userPerms) { + return nil + } + + if hasContextResourcePermission(userPerms, resourceID) { + return nil + } + + return errors.ThrowPermissionDenied(nil, "AUTH-Swrgg2", "No matching permissions found") +} + +func hasContextResourcePermission(permissions []string, resourceID string) bool { + for _, perm := range permissions { + _, ctxID := SplitPermission(perm) + if resourceID == ctxID { + return true + } + } + return false +} + +func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, roleMappings []RoleMapping) (requestPermissions, allPermissions []string) { requestPermissions = make([]string, 0) allPermissions = make([]string, 0) for _, membership := range memberships { - requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, authConfig, requestPermissions, allPermissions) + requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, requestPermissions, allPermissions) } return requestPermissions, allPermissions } -func mapMembershipToPerm(requiredPerm string, membership *Membership, authConfig Config, requestPermissions, allPermissions []string) ([]string, []string) { +func mapMembershipToPerm(requiredPerm string, membership *Membership, roleMappings []RoleMapping, requestPermissions, allPermissions []string) ([]string, []string) { roleNames, roleContextID := roleWithContext(membership) for _, roleName := range roleNames { - perms := authConfig.getPermissionsFromRole(roleName) + perms := getPermissionsFromRole(roleMappings, roleName) for _, p := range perms { permWithCtx := addRoleContextIDToPerm(p, roleContextID) diff --git a/internal/api/authz/permissions_test.go b/internal/api/authz/permissions_test.go index 1a1b97d6f8..ea217e1b85 100644 --- a/internal/api/authz/permissions_test.go +++ b/internal/api/authz/permissions_test.go @@ -18,7 +18,7 @@ type testVerifier struct { func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "userID", "agentID", "clientID", "de", "orgID", nil } -func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*Membership, error) { +func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) { return v.memberships, nil } @@ -46,7 +46,7 @@ func equalStringArray(a, b []string) bool { return true } -func Test_GetUserMethodPermissions(t *testing.T) { +func Test_GetUserPermissions(t *testing.T) { type args struct { ctxData CtxData verifier *TokenVerifier @@ -139,7 +139,7 @@ func Test_GetUserMethodPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, perms, err := getUserMethodPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig, tt.args.ctxData) + _, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID) if tt.wantErr && err == nil { t.Errorf("got wrong result, should get err: actual: %v ", err) @@ -295,7 +295,7 @@ func Test_MapMembershipToPermissions(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig) + requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings) if !equalStringArray(requestPerms, tt.requestPerms) { t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms) } @@ -435,7 +435,7 @@ func Test_MapMembershipToPerm(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig, tt.args.requestPerms, tt.args.allPerms) + requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings, tt.args.requestPerms, tt.args.allPerms) if !equalStringArray(requestPerms, tt.requestPerms) { t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms) } @@ -519,3 +519,109 @@ func Test_ExistisPerm(t *testing.T) { }) } } + +func Test_CheckUserResourcePermissions(t *testing.T) { + type args struct { + perms []string + resourceID string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "no permissions", + args: args{ + perms: []string{}, + resourceID: "", + }, + wantErr: true, + }, + { + name: "has permission and no context requested", + args: args{ + perms: []string{"project.read"}, + resourceID: "", + }, + wantErr: false, + }, + { + name: "context requested and has global permission", + args: args{ + perms: []string{"project.read", "project.read:1"}, + resourceID: "Test", + }, + wantErr: false, + }, + { + name: "context requested and has specific permission", + args: args{ + perms: []string{"project.read:Test"}, + resourceID: "Test", + }, + wantErr: false, + }, + { + name: "context requested and has no permission", + args: args{ + perms: []string{"project.read:Test"}, + resourceID: "Hodor", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := checkUserResourcePermissions(tt.args.perms, tt.args.resourceID) + if tt.wantErr && err == nil { + t.Errorf("got wrong result, should get err: actual: %v ", err) + } + + if !tt.wantErr && err != nil { + t.Errorf("shouldn't get err: %v ", err) + } + + if tt.wantErr && !caos_errs.IsPermissionDenied(err) { + t.Errorf("got wrong err: %v ", err) + } + }) + } +} + +func Test_HasContextResourcePermission(t *testing.T) { + type args struct { + perms []string + resourceID string + } + tests := []struct { + name string + args args + result bool + }{ + { + name: "existing context permission", + args: args{ + perms: []string{"test:wrong", "test:right"}, + resourceID: "right", + }, + result: true, + }, + { + name: "not existing context permission", + args: args{ + perms: []string{"test:wrong", "test:wrong2"}, + resourceID: "test", + }, + result: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasContextResourcePermission(tt.args.perms, tt.args.resourceID) + if result != tt.result { + t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) + } + }) + } +} diff --git a/internal/api/authz/token.go b/internal/api/authz/token.go index a7ea46a685..a8a9b3afb1 100644 --- a/internal/api/authz/token.go +++ b/internal/api/authz/token.go @@ -27,10 +27,14 @@ type TokenVerifier struct { systemJWTProfile op.JWTProfileVerifier } +type MembershipsResolver interface { + SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) +} + type authZRepo interface { VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error) VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error) - SearchMyMemberships(ctx context.Context) ([]*Membership, error) + SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) ExistsOrg(ctx context.Context, orgID string) error } @@ -127,10 +131,10 @@ func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings Me } } -func (v *TokenVerifier) SearchMyMemberships(ctx context.Context) (_ []*Membership, err error) { +func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string) (_ []*Membership, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - return v.authZRepo.SearchMyMemberships(ctx) + return v.authZRepo.SearchMyMemberships(ctx, orgID) } func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) { diff --git a/internal/api/grpc/admin/server.go b/internal/api/grpc/admin/server.go index 267fad2f45..d25bd4b3d4 100644 --- a/internal/api/grpc/admin/server.go +++ b/internal/api/grpc/admin/server.go @@ -71,7 +71,7 @@ func (s *Server) AppName() string { } func (s *Server) MethodPrefix() string { - return admin.AdminService_MethodPrefix + return admin.AdminService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { diff --git a/internal/api/grpc/auth/server.go b/internal/api/grpc/auth/server.go index ff7d7ae7b6..16fb66022f 100644 --- a/internal/api/grpc/auth/server.go +++ b/internal/api/grpc/auth/server.go @@ -69,7 +69,7 @@ func (s *Server) AppName() string { } func (s *Server) MethodPrefix() string { - return auth.AuthService_MethodPrefix + return auth.AuthService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { diff --git a/internal/api/grpc/management/server.go b/internal/api/grpc/management/server.go index 732f44ea29..12413e1bc8 100644 --- a/internal/api/grpc/management/server.go +++ b/internal/api/grpc/management/server.go @@ -63,7 +63,7 @@ func (s *Server) AppName() string { } func (s *Server) MethodPrefix() string { - return management.ManagementService_MethodPrefix + return management.ManagementService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { diff --git a/internal/api/grpc/management/user.go b/internal/api/grpc/management/user.go index 58092dc739..4c81243fb6 100644 --- a/internal/api/grpc/management/user.go +++ b/internal/api/grpc/management/user.go @@ -210,17 +210,14 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe } func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) { - details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req)) + human := AddHumanUserRequestToAddHuman(req) + err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true) if err != nil { return nil, err } return &mgmt_pb.AddHumanUserResponse{ - UserId: details.ID, - Details: obj_grpc.AddToDetailsPb( - details.Sequence, - details.EventDate, - details.ResourceOwner, - ), + UserId: human.ID, + Details: obj_grpc.DomainToAddDetailsPb(human.Details), }, nil } diff --git a/internal/api/grpc/object/v2/converter.go b/internal/api/grpc/object/v2/converter.go new file mode 100644 index 0000000000..eca168f70c --- /dev/null +++ b/internal/api/grpc/object/v2/converter.go @@ -0,0 +1,19 @@ +package object + +import ( + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" +) + +func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details { + details := &object.Details{ + Sequence: objectDetail.Sequence, + ResourceOwner: objectDetail.ResourceOwner, + } + if !objectDetail.EventDate.IsZero() { + details.ChangeDate = timestamppb.New(objectDetail.EventDate) + } + return details +} diff --git a/internal/api/grpc/project/application.go b/internal/api/grpc/project/application.go index 5565ba359e..e555e93d27 100644 --- a/internal/api/grpc/project/application.go +++ b/internal/api/grpc/project/application.go @@ -136,6 +136,8 @@ func OIDCGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app_pb.OIDCGra oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT case domain.OIDCGrantTypeRefreshToken: oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN + case domain.OIDCGrantTypeDeviceCode: + oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE } } return oidcGrantTypes @@ -154,6 +156,8 @@ func OIDCGrantTypesToDomain(grantTypes []app_pb.OIDCGrantType) []domain.OIDCGran oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN: oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken + case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE: + oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode } } return oidcGrantTypes diff --git a/internal/api/grpc/server/middleware/auth_interceptor.go b/internal/api/grpc/server/middleware/auth_interceptor.go index e1c6e6e02d..f02a822d86 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor.go +++ b/internal/api/grpc/server/middleware/auth_interceptor.go @@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, } orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID) + if o, ok := req.(AuthContext); ok { + orgID = o.AuthContext() + } ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod) if err != nil { @@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, span.End() return handler(ctxSetter(ctx), req) } + +type AuthContext interface { + AuthContext() string +} diff --git a/internal/api/grpc/server/middleware/auth_interceptor_test.go b/internal/api/grpc/server/middleware/auth_interceptor_test.go index abe50606f9..c041d9ca47 100644 --- a/internal/api/grpc/server/middleware/auth_interceptor_test.go +++ b/internal/api/grpc/server/middleware/auth_interceptor_test.go @@ -24,7 +24,7 @@ type verifierMock struct{} func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) { return "", "", "", "", "", nil } -func (v *verifierMock) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) { +func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error) { return nil, nil } diff --git a/internal/api/grpc/server/server.go b/internal/api/grpc/server/server.go index 71eef0afa6..9252778036 100644 --- a/internal/api/grpc/server/server.go +++ b/internal/api/grpc/server/server.go @@ -50,13 +50,13 @@ func CreateServer( middleware.MetricsHandler(metricTypes, grpc_api.Probes...), middleware.NoCacheInterceptor(), middleware.ErrorHandler(), - middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_MethodPrefix, healthpb.Health_ServiceDesc.ServiceName), + middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName), middleware.AccessStorageInterceptor(accessSvc), middleware.AuthorizationInterceptor(verifier, authConfig), middleware.TranslationHandler(), middleware.ValidationHandler(), middleware.ServiceHandler(), - middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_MethodPrefix), + middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName), ), ), } diff --git a/internal/api/grpc/system/server.go b/internal/api/grpc/system/server.go index a954cfce43..dd947c950f 100644 --- a/internal/api/grpc/system/server.go +++ b/internal/api/grpc/system/server.go @@ -4,7 +4,6 @@ import ( "google.golang.org/grpc" "github.com/zitadel/zitadel/internal/admin/repository" - "github.com/zitadel/zitadel/internal/admin/repository/eventsourcing" "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" @@ -60,7 +59,7 @@ func (s *Server) AppName() string { } func (s *Server) MethodPrefix() string { - return system.SystemService_MethodPrefix + return system.SystemService_ServiceDesc.ServiceName } func (s *Server) AuthMethods() authz.MethodMapping { diff --git a/internal/api/grpc/user/v2/email.go b/internal/api/grpc/user/v2/email.go new file mode 100644 index 0000000000..95928e2e61 --- /dev/null +++ b/internal/api/grpc/user/v2/email.go @@ -0,0 +1,65 @@ +package user + +import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) { + var resourceOwner string // TODO: check if still needed + var email *domain.Email + + switch v := req.GetVerification().(type) { + case *user.SetEmailRequest_SendCode: + email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate()) + case *user.SetEmailRequest_ReturnCode: + email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg) + case *user.SetEmailRequest_IsVerified: + if v.IsVerified { + email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail()) + } else { + email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg) + } + case nil: + email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg) + default: + err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v) + } + if err != nil { + return nil, err + } + + return &user.SetEmailResponse{ + Details: &object.Details{ + Sequence: email.Sequence, + ChangeDate: timestamppb.New(email.ChangeDate), + ResourceOwner: email.ResourceOwner, + }, + VerificationCode: email.PlainCode, + }, nil +} + +func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) { + details, err := s.command.VerifyUserEmail(ctx, + req.GetUserId(), + "", // TODO: check if still needed + req.GetVerificationCode(), + s.userCodeAlg, + ) + if err != nil { + return nil, err + } + return &user.VerifyEmailResponse{ + Details: &object.Details{ + Sequence: details.Sequence, + ChangeDate: timestamppb.New(details.EventDate), + ResourceOwner: details.ResourceOwner, + }, + }, nil +} diff --git a/internal/api/grpc/user/v2/server.go b/internal/api/grpc/user/v2/server.go index 6a7a96d31d..d81d51ff23 100644 --- a/internal/api/grpc/user/v2/server.go +++ b/internal/api/grpc/user/v2/server.go @@ -6,27 +6,27 @@ import ( "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/api/grpc/server" "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/query" - "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" ) var _ user.UserServiceServer = (*Server)(nil) type Server struct { user.UnimplementedUserServiceServer - command *command.Commands - query *query.Queries + command *command.Commands + query *query.Queries + userCodeAlg crypto.EncryptionAlgorithm } type Config struct{} -func CreateServer( - command *command.Commands, - query *query.Queries, -) *Server { +func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server { return &Server{ - command: command, - query: query, + command: command, + query: query, + userCodeAlg: userCodeAlg, } } diff --git a/internal/api/grpc/user/v2/test.go b/internal/api/grpc/user/v2/test.go deleted file mode 100644 index 8b5bc38654..0000000000 --- a/internal/api/grpc/user/v2/test.go +++ /dev/null @@ -1,55 +0,0 @@ -package user - -import ( - "context" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/errors" - "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" -) - -func (s *Server) TestGet(ctx context.Context, req *user.TestGetRequest) (*user.TestGetResponse, error) { - return &user.TestGetResponse{ - Ctx: req.Ctx.String(), - }, nil -} - -func (s *Server) TestPost(ctx context.Context, req *user.TestPostRequest) (*user.TestPostResponse, error) { - return &user.TestPostResponse{ - Ctx: req.Ctx.String(), - }, nil -} - -func (s *Server) TestAuth(ctx context.Context, req *user.TestAuthRequest) (*user.TestAuthResponse, error) { - reqCtx, err := authDemo(ctx, req.Ctx) - if err != nil { - return nil, err - } - return &user.TestAuthResponse{ - User: &user.User{Id: authz.GetCtxData(ctx).UserID}, - Ctx: reqCtx, - }, nil -} - -func authDemo(ctx context.Context, reqCtx *user.Context) (*user.Context, error) { - ro := authz.GetCtxData(ctx).ResourceOwner - if reqCtx == nil { - return &user.Context{Ctx: &user.Context_OrgId{OrgId: ro}}, nil - } - switch c := reqCtx.Ctx.(type) { - case *user.Context_OrgId: - if c.OrgId == ro { - return reqCtx, nil - } - return nil, errors.ThrowPermissionDenied(nil, "USER-dg4g", "Errors.User.NotAllowedOrg") - case *user.Context_OrgDomain: - if c.OrgDomain == "forbidden.com" { - return nil, errors.ThrowPermissionDenied(nil, "USER-SDg4g", "Errors.User.NotAllowedOrg") - } - return reqCtx, nil - case *user.Context_Instance: - return reqCtx, nil - default: - return reqCtx, nil - } -} diff --git a/internal/api/grpc/user/v2/user.go b/internal/api/grpc/user/v2/user.go new file mode 100644 index 0000000000..02b7e06282 --- /dev/null +++ b/internal/api/grpc/user/v2/user.go @@ -0,0 +1,112 @@ +package user + +import ( + "context" + "io" + + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/grpc/object/v2" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) { + human, err := addUserRequestToAddHuman(req) + if err != nil { + return nil, err + } + orgID := req.GetOrganisation().GetOrgId() + if orgID == "" { + orgID = authz.GetCtxData(ctx).OrgID + } + err = s.command.AddHuman(ctx, orgID, human, false) + if err != nil { + return nil, err + } + return &user.AddHumanUserResponse{ + UserId: human.ID, + Details: object.DomainToDetailsPb(human.Details), + EmailCode: human.EmailCode, + }, nil +} + +func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) { + username := req.GetUsername() + if username == "" { + username = req.GetEmail().GetEmail() + } + var urlTemplate string + if req.GetEmail().GetSendCode() != nil { + urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate() + // test the template execution so the async notification will not fail because of it and the user won't realize + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil { + return nil, err + } + } + bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword()) + if err != nil { + return nil, err + } + passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired() + metadata := make([]*command.AddMetadataEntry, len(req.Metadata)) + for i, metadataEntry := range req.Metadata { + metadata[i] = &command.AddMetadataEntry{ + Key: metadataEntry.GetKey(), + Value: metadataEntry.GetValue(), + } + } + return &command.AddHuman{ + ID: req.GetUserId(), + Username: username, + FirstName: req.GetProfile().GetFirstName(), + LastName: req.GetProfile().GetLastName(), + NickName: req.GetProfile().GetNickName(), + DisplayName: req.GetProfile().GetDisplayName(), + Email: command.Email{ + Address: domain.EmailAddress(req.GetEmail().GetEmail()), + Verified: req.GetEmail().GetIsVerified(), + ReturnCode: req.GetEmail().GetReturnCode() != nil, + URLTemplate: urlTemplate, + }, + PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()), + Gender: genderToDomain(req.GetProfile().GetGender()), + Phone: command.Phone{}, // TODO: add as soon as possible + Password: req.GetPassword().GetPassword(), + BcryptedPassword: bcryptedPassword, + PasswordChangeRequired: passwordChangeRequired, + Passwordless: false, + ExternalIDP: false, + Register: false, + Metadata: metadata, + }, nil +} + +func genderToDomain(gender user.Gender) domain.Gender { + switch gender { + case user.Gender_GENDER_UNSPECIFIED: + return domain.GenderUnspecified + case user.Gender_GENDER_FEMALE: + return domain.GenderFemale + case user.Gender_GENDER_MALE: + return domain.GenderMale + case user.Gender_GENDER_DIVERSE: + return domain.GenderDiverse + default: + return domain.GenderUnspecified + } +} + +func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) { + if hashed == nil { + return "", nil + } + // we currently only handle bcrypt + if hashed.GetAlgorithm() != "bcrypt" { + return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument") + } + return hashed.GetHash(), nil +} diff --git a/internal/api/grpc/user/v2/user_test.go b/internal/api/grpc/user/v2/user_test.go new file mode 100644 index 0000000000..d697e9ae4f --- /dev/null +++ b/internal/api/grpc/user/v2/user_test.go @@ -0,0 +1,80 @@ +package user + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + caos_errs "github.com/zitadel/zitadel/internal/errors" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" +) + +func Test_hashedPasswordToCommand(t *testing.T) { + type args struct { + hashed *user.HashedPassword + } + type res struct { + want string + err func(error) bool + } + tests := []struct { + name string + args args + res res + }{ + { + "not hashed", + args{ + hashed: nil, + }, + res{ + "", + nil, + }, + }, + { + "hashed, not bcrypt", + args{ + hashed: &user.HashedPassword{ + Hash: "hash", + Algorithm: "custom", + }, + }, + res{ + "", + func(err error) bool { + return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")) + }, + }, + }, + { + "hashed, bcrypt", + args{ + hashed: &user.HashedPassword{ + Hash: "hash", + Algorithm: "bcrypt", + }, + }, + res{ + "hash", + nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := hashedPasswordToCommand(tt.args.hashed) + if tt.res.err == nil { + require.NoError(t, err) + } + if tt.res.err != nil && !tt.res.err(err) { + t.Errorf("got wrong err: %v ", err) + } + if tt.res.err == nil { + assert.Equal(t, tt.res.want, got) + } + }) + } +} diff --git a/internal/api/oidc/auth_request_converter.go b/internal/api/oidc/auth_request_converter.go index 48729705e7..6473460843 100644 --- a/internal/api/oidc/auth_request_converter.go +++ b/internal/api/oidc/auth_request_converter.go @@ -99,15 +99,6 @@ func (a *AuthRequest) GetSubject() string { return a.UserID } -func (a *AuthRequest) Done() bool { - for _, step := range a.PossibleSteps { - if step.Type() == domain.NextStepRedirectToCallback { - return true - } - } - return false -} - func (a *AuthRequest) oidc() *domain.AuthRequestOIDC { return a.Request.(*domain.AuthRequestOIDC) } diff --git a/internal/api/oidc/client_converter.go b/internal/api/oidc/client_converter.go index 749a5c3dff..6b32f38927 100644 --- a/internal/api/oidc/client_converter.go +++ b/internal/api/oidc/client_converter.go @@ -200,6 +200,8 @@ func grantTypeToOIDC(grantType domain.OIDCGrantType) oidc.GrantType { return oidc.GrantTypeImplicit case domain.OIDCGrantTypeRefreshToken: return oidc.GrantTypeRefreshToken + case domain.OIDCGrantTypeDeviceCode: + return oidc.GrantTypeDeviceCode default: return oidc.GrantTypeCode } diff --git a/internal/api/oidc/device_auth.go b/internal/api/oidc/device_auth.go new file mode 100644 index 0000000000..7eee06096a --- /dev/null +++ b/internal/api/oidc/device_auth.go @@ -0,0 +1,176 @@ +package oidc + +import ( + "context" + "time" + + "github.com/zitadel/logging" + "github.com/zitadel/oidc/v2/pkg/oidc" + "github.com/zitadel/oidc/v2/pkg/op" + + "github.com/zitadel/zitadel/internal/api/ui/login" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +const ( + DeviceAuthDefaultLifetime = 5 * time.Minute + DeviceAuthDefaultPollInterval = 5 * time.Second +) + +type DeviceAuthorizationConfig struct { + Lifetime time.Duration + PollInterval time.Duration + UserCode *UserCodeConfig +} + +type UserCodeConfig struct { + CharSet string + CharAmount int + DashInterval int +} + +// toOPConfig converts DeviceAuthorizationConfig to a [op.DeviceAuthorizationConfig], +// setting sane defaults for empty values. +// Safe to call when c is nil. +func (c *DeviceAuthorizationConfig) toOPConfig() op.DeviceAuthorizationConfig { + out := op.DeviceAuthorizationConfig{ + Lifetime: DeviceAuthDefaultLifetime, + PollInterval: DeviceAuthDefaultPollInterval, + UserFormPath: login.EndpointDeviceAuth, + UserCode: op.UserCodeBase20, + } + if c == nil { + return out + } + if c.Lifetime != 0 { + out.Lifetime = c.Lifetime + } + if c.PollInterval != 0 { + out.PollInterval = c.PollInterval + } + + if c.UserCode == nil { + return out + } + if c.UserCode.CharSet != "" { + out.UserCode.CharSet = c.UserCode.CharSet + } + if c.UserCode.CharAmount != 0 { + out.UserCode.CharAmount = c.UserCode.CharAmount + } + if c.UserCode.DashInterval != 0 { + out.UserCode.DashInterval = c.UserCode.CharAmount + } + return out +} + +// StoreDeviceAuthorization creates a new Device Authorization request. +// Implements the op.DeviceAuthorizationStorage interface. +func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (err error) { + const logMsg = "store device authorization" + logger := logging.WithFields("client_id", clientID, "device_code", deviceCode, "user_code", userCode, "expires", expires, "scopes", scopes) + + ctx, span := tracing.NewSpan(ctx) + defer func() { + logger.OnError(err).Error(logMsg) + span.EndWithError(err) + }() + + // TODO(muhlemmer): Remove the following code block with oidc v3 + // https://github.com/zitadel/oidc/issues/370 + client, err := o.GetClientByClientID(ctx, clientID) + if err != nil { + return err + } + if !op.ValidateGrantType(client, oidc.GrantTypeDeviceCode) { + return errors.ThrowPermissionDeniedf(nil, "OIDC-et1Ae", "grant type %q not allowed for client", oidc.GrantTypeDeviceCode) + } + + scopes, err = o.assertProjectRoleScopes(ctx, clientID, scopes) + if err != nil { + return errors.ThrowPreconditionFailed(err, "OIDC-She4t", "Errors.Internal") + } + aggrID, details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scopes) + if err == nil { + logger.SetFields("aggregate_id", aggrID, "details", details).Debug(logMsg) + } + + return err +} + +func newDeviceAuthorizationState(d *domain.DeviceAuth) *op.DeviceAuthorizationState { + return &op.DeviceAuthorizationState{ + ClientID: d.ClientID, + Scopes: d.Scopes, + Expires: d.Expires, + Done: d.State.Done(), + Subject: d.Subject, + Denied: d.State.Denied(), + } +} + +// GetDeviceAuthorizatonState retieves the current state of the Device Authorization process. +// It implements the [op.DeviceAuthorizationStorage] interface and is used by devices that +// are polling until they successfully receive a token or we indicate a denied or expired state. +// As generated user codes are of low entropy, this implementation also takes care or +// device authorization request cleanup, when it has been Approved, Denied or Expired. +func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (state *op.DeviceAuthorizationState, err error) { + const logMsg = "get device authorization state" + logger := logging.WithFields("client_id", clientID, "device_code", deviceCode) + + ctx, span := tracing.NewSpan(ctx) + defer func() { + if err != nil { + logger.WithError(err).Error(logMsg) + } + span.EndWithError(err) + }() + + deviceAuth, err := o.query.DeviceAuthByDeviceCode(ctx, clientID, deviceCode) + if err != nil { + return nil, err + } + logger.SetFields( + "expires", deviceAuth.Expires, "scopes", deviceAuth.Scopes, + "subject", deviceAuth.Subject, "state", deviceAuth.State, + ).Debug("device authorization state") + + // Cancel the request if it is expired, only if it wasn't Done meanwhile + if !deviceAuth.State.Done() && deviceAuth.Expires.Before(time.Now()) { + _, err = o.command.CancelDeviceAuth(ctx, deviceAuth.AggregateID, domain.DeviceAuthCanceledExpired) + if err != nil { + return nil, err + } + deviceAuth.State = domain.DeviceAuthStateExpired + } + + // When the request is more then initiated, it has been either Approved, Denied or Expired. + // At this point we should remove it from the DB to avoid user code conflicts. + if deviceAuth.State > domain.DeviceAuthStateInitiated { + _, err = o.command.RemoveDeviceAuth(ctx, deviceAuth.AggregateID) + if err != nil { + return nil, err + } + } + + return newDeviceAuthorizationState(deviceAuth), nil +} + +// TODO(muhlemmer): remove the following methods with oidc v3. +// They are actually not used, but are required by the oidc device storage interface. +// https://github.com/zitadel/oidc/issues/371 +func (o *OPStorage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) { + return nil, nil +} + +func (o *OPStorage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) (err error) { + return nil +} + +func (o *OPStorage) DenyDeviceAuthorization(ctx context.Context, userCode string) (err error) { + return nil +} + +// TODO end. diff --git a/internal/api/oidc/key.go b/internal/api/oidc/key.go index 3b4f73c83f..237af7db49 100644 --- a/internal/api/oidc/key.go +++ b/internal/api/oidc/key.go @@ -176,7 +176,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm if errors.IsErrorAlreadyExists(err) { return nil } - logging.OnError(err).Warn("initial lock failed") + logging.OnError(err).Debug("initial lock failed") return err } diff --git a/internal/api/oidc/op.go b/internal/api/oidc/op.go index 48167f402c..9574f561b6 100644 --- a/internal/api/oidc/op.go +++ b/internal/api/oidc/op.go @@ -40,6 +40,7 @@ type Config struct { UserAgentCookieConfig *middleware.UserAgentCookieConfig Cache *middleware.CacheConfig CustomEndpoints *EndpointConfig + DeviceAuth *DeviceAuthorizationConfig } type EndpointConfig struct { @@ -50,6 +51,7 @@ type EndpointConfig struct { Revocation *Endpoint EndSession *Endpoint Keys *Endpoint + DeviceAuth *Endpoint } type Endpoint struct { @@ -108,6 +110,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey [] GrantTypeRefreshToken: config.GrantTypeRefreshToken, RequestObjectSupported: config.RequestObjectSupported, SupportedUILocales: supportedLanguages, + DeviceAuthorization: config.DeviceAuth.toOPConfig(), } if cryptoLength := len(cryptoKey); cryptoLength != 32 { return nil, caos_errs.ThrowInternalf(nil, "OIDC-D43gf", "crypto key must be 32 bytes, but is %d", cryptoLength) @@ -165,6 +168,9 @@ func customEndpoints(endpointConfig *EndpointConfig) []op.Option { if endpointConfig.Keys != nil { options = append(options, op.WithCustomKeysEndpoint(op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL))) } + if endpointConfig.DeviceAuth != nil { + options = append(options, op.WithCustomDeviceAuthorizationEndpoint(op.NewEndpointWithURL(endpointConfig.DeviceAuth.Path, endpointConfig.DeviceAuth.URL))) + } return options } diff --git a/internal/api/saml/auth_request_converter.go b/internal/api/saml/auth_request_converter.go index a19fed0920..28f2a3c548 100644 --- a/internal/api/saml/auth_request_converter.go +++ b/internal/api/saml/auth_request_converter.go @@ -63,14 +63,6 @@ func (a *AuthRequest) GetUserID() string { func (a *AuthRequest) GetUserName() string { return a.UserName } -func (a *AuthRequest) Done() bool { - for _, step := range a.PossibleSteps { - if step.Type() == domain.NextStepRedirectToCallback { - return true - } - } - return false -} func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ models.AuthRequestInt, err error) { if _, ok := authReq.Request.(*domain.AuthRequestSAML); !ok { diff --git a/internal/api/saml/certificate.go b/internal/api/saml/certificate.go index 8d34aeb4cf..9e85e50982 100644 --- a/internal/api/saml/certificate.go +++ b/internal/api/saml/certificate.go @@ -123,7 +123,7 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do if errors.IsErrorAlreadyExists(err) { return nil } - logging.OnError(err).Warn("initial lock failed") + logging.OnError(err).Debug("initial lock failed") return err } diff --git a/internal/api/ui/login/device_auth.go b/internal/api/ui/login/device_auth.go new file mode 100644 index 0000000000..e2322ee04f --- /dev/null +++ b/internal/api/ui/login/device_auth.go @@ -0,0 +1,201 @@ +package login + +import ( + errs "errors" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/gorilla/mux" + "github.com/muhlemmer/gu" + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/api/http/middleware" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" +) + +const ( + tmplDeviceAuthUserCode = "device-usercode" + tmplDeviceAuthAction = "device-action" +) + +func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, err error) { + var errID, errMessage string + if err != nil { + logging.WithError(err).Error() + errID, errMessage = l.getErrorMessage(r, err) + } + + data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage) + translator := l.getTranslator(r.Context(), nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil) +} + +func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) { + data := &struct { + baseData + AuthRequestID string + Username string + ClientID string + Scopes []string + }{ + baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""), + AuthRequestID: authReq.ID, + Username: authReq.UserName, + ClientID: authReq.ApplicationID, + Scopes: scopes, + } + + translator := l.getTranslator(r.Context(), authReq) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil) +} + +const ( + deviceAuthAllowed = "allowed" + deviceAuthDenied = "denied" +) + +// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied. +func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) { + data := &struct { + baseData + Message string + }{ + baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""), + } + + translator := l.getTranslator(r.Context(), authReq) + switch action { + case deviceAuthAllowed: + data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplSuccess], data, nil) + case deviceAuthDenied: + data.ErrMessage = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Denied", nil) + l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil) + } +} + +// handleDeviceUserCode serves the Device Authorization user code submission form. +// The "user_code" may be submitted by URL (GET) or form (POST). +// When a "user_code" is received and found through query, +// handleDeviceAuthUserCode will create a new AuthRequest in the repository. +// The user is then redirected to the /login endpoint to complete authentication. +// +// The agent ID from the context is set to the authentication request +// to ensure the complete login flow is completed from the same browser. +func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + err := r.ParseForm() + if err != nil { + w.WriteHeader(http.StatusBadRequest) + l.renderDeviceAuthUserCode(w, r, err) + return + } + userCode := r.Form.Get("user_code") + if userCode == "" { + if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" { + err = errs.New(prompt) + } + l.renderDeviceAuthUserCode(w, r, err) + return + } + deviceAuth, err := l.query.DeviceAuthByUserCode(ctx, userCode) + if err != nil { + l.renderDeviceAuthUserCode(w, r, err) + return + } + userAgentID, ok := middleware.UserAgentIDFromCtx(ctx) + if !ok { + l.renderDeviceAuthUserCode(w, r, errs.New("internal error: agent ID missing")) + return + } + authRequest, err := l.authRepo.CreateAuthRequest(ctx, &domain.AuthRequest{ + CreationDate: time.Now(), + AgentID: userAgentID, + ApplicationID: deviceAuth.ClientID, + InstanceID: authz.GetInstance(ctx).InstanceID(), + Request: &domain.AuthRequestDevice{ + ID: deviceAuth.AggregateID, + DeviceCode: deviceAuth.DeviceCode, + UserCode: deviceAuth.UserCode, + Scopes: deviceAuth.Scopes, + }, + }) + if err != nil { + l.renderDeviceAuthUserCode(w, r, err) + return + } + + http.Redirect(w, r, l.renderer.pathPrefix+EndpointLogin+"?authRequestID="+authRequest.ID, http.StatusFound) +} + +// redirectDeviceAuthStart redirects the user to the start point of +// the device authorization flow. A prompt can be set to inform the user +// of the reason why they are redirected back. +func (l *Login) redirectDeviceAuthStart(w http.ResponseWriter, r *http.Request, prompt string) { + values := make(url.Values) + values.Set("prompt", url.QueryEscape(prompt)) + + url := url.URL{ + Path: l.renderer.pathPrefix + EndpointDeviceAuth, + RawQuery: values.Encode(), + } + http.Redirect(w, r, url.String(), http.StatusSeeOther) +} + +// handleDeviceAuthAction is the handler where the user is redirected after login. +// The authRequest is checked if the login was indeed completed. +// When the action of "allowed" or "denied", the device authorization is updated accordingly. +// Else the user is presented with a page where they can choose / submit either action. +func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) { + authReq, err := l.getAuthRequest(r) + if authReq == nil { + err = errors.ThrowInvalidArgument(err, "LOGIN-OLah8", "invalid or missing auth request") + l.redirectDeviceAuthStart(w, r, err.Error()) + return + } + if !authReq.Done() { + l.redirectDeviceAuthStart(w, r, "authentication not completed") + return + } + authDev, ok := authReq.Request.(*domain.AuthRequestDevice) + if !ok { + l.redirectDeviceAuthStart(w, r, fmt.Sprintf("wrong auth request type: %T", authReq.Request)) + return + } + + action := mux.Vars(r)["action"] + switch action { + case deviceAuthAllowed: + _, err = l.command.ApproveDeviceAuth(r.Context(), authDev.ID, authReq.UserID) + case deviceAuthDenied: + _, err = l.command.CancelDeviceAuth(r.Context(), authDev.ID, domain.DeviceAuthCanceledDenied) + default: + l.renderDeviceAuthAction(w, r, authReq, authDev.Scopes) + return + } + if err != nil { + l.redirectDeviceAuthStart(w, r, err.Error()) + return + } + + l.renderDeviceAuthDone(w, r, authReq, action) +} + +// deviceAuthCallbackURL creates the callback URL with which the user +// is redirected back to the device authorization flow. +func (l *Login) deviceAuthCallbackURL(authRequestID string) string { + return l.renderer.pathPrefix + EndpointDeviceAuthAction + "?authRequestID=" + authRequestID +} + +// RedirectDeviceAuthToPrefix allows users to use https://domain.com/device without the /ui/login prefix +// and redirects them to the prefixed endpoint. +// [rfc 8628](https://www.rfc-editor.org/rfc/rfc8628#section-3.2) recommends the URL to be as short as possible. +func RedirectDeviceAuthToPrefix(w http.ResponseWriter, r *http.Request) { + target := gu.PtrCopy(r.URL) + target.Path = HandlerPrefix + EndpointDeviceAuth + http.Redirect(w, r, target.String(), http.StatusFound) +} diff --git a/internal/api/ui/login/login_success_handler.go b/internal/api/ui/login/login_success_handler.go index aad9a67393..f05ee48185 100644 --- a/internal/api/ui/login/login_success_handler.go +++ b/internal/api/ui/login/login_success_handler.go @@ -69,6 +69,8 @@ func (l *Login) authRequestCallback(ctx context.Context, authReq *domain.AuthReq return l.oidcAuthCallbackURL(ctx, authReq.ID), nil case *domain.AuthRequestSAML: return l.samlAuthCallbackURL(ctx, authReq.ID), nil + case *domain.AuthRequestDevice: + return l.deviceAuthCallbackURL(authReq.ID), nil default: return "", caos_errs.ThrowInternal(nil, "LOGIN-rhjQF", "Errors.AuthRequest.RequestTypeNotSupported") } diff --git a/internal/api/ui/login/renderer.go b/internal/api/ui/login/renderer.go index 8266b74d07..a9b12f19dc 100644 --- a/internal/api/ui/login/renderer.go +++ b/internal/api/ui/login/renderer.go @@ -25,7 +25,8 @@ import ( ) const ( - tmplError = "error" + tmplError = "error" + tmplSuccess = "success" ) type Renderer struct { @@ -45,6 +46,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage } tmplMapping := map[string]string{ tmplError: "error.html", + tmplSuccess: "success.html", tmplLogin: "login.html", tmplUserSelection: "select_user.html", tmplPassword: "password.html", @@ -77,6 +79,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage tmplExternalNotFoundOption: "external_not_found_option.html", tmplLoginSuccess: "login_success.html", tmplLDAPLogin: "ldap_login.html", + tmplDeviceAuthUserCode: "device_usercode.html", + tmplDeviceAuthAction: "device_action.html", } funcs := map[string]interface{}{ "resourceUrl": func(file string) string { @@ -323,6 +327,7 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq * func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) { var msg string if err != nil { + logging.WithError(err).WithField("auth_req_id", authReq.ID).Error() _, msg = l.getErrorMessage(r, err) } data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg) diff --git a/internal/api/ui/login/router.go b/internal/api/ui/login/router.go index e723cad1ac..8ad27d7573 100644 --- a/internal/api/ui/login/router.go +++ b/internal/api/ui/login/router.go @@ -46,6 +46,9 @@ const ( EndpointResources = "/resources" EndpointDynamicResources = "/resources/dynamic" + + EndpointDeviceAuth = "/device" + EndpointDeviceAuthAction = "/device/{action}" ) var ( @@ -107,5 +110,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M router.HandleFunc(EndpointLDAPLogin, login.handleLDAP).Methods(http.MethodGet) router.HandleFunc(EndpointLDAPCallback, login.handleLDAPCallback).Methods(http.MethodPost) router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently)) + router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost) + router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost) return router } diff --git a/internal/api/ui/login/static/i18n/de.yaml b/internal/api/ui/login/static/i18n/de.yaml index 5b12002119..96a5d461fa 100644 --- a/internal/api/ui/login/static/i18n/de.yaml +++ b/internal/api/ui/login/static/i18n/de.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: Geräteautorisierung + UserCode: + Label: Benutzercode + Description: Geben Sie den auf dem Gerät angezeigten Benutzercode ein + ButtonNext: weiter + Action: + Description: Gerätezugriff erlauben + GrantDevice: Sie sind dabei, das Gerät zu erlauben + AccessToScopes: Zugriff auf die folgenden Daten + Button: + Allow: erlauben + Deny: verweigern + Done: + Description: Abgeschlossen + Approved: Gerätezulassung genehmigt. Sie können jetzt zum Gerät zurückkehren. + Denied: Geräteautorisierung verweigert. Sie können jetzt zum Gerät zurückkehren. + Footer: PoweredBy: Powered By Tos: AGB @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: Registrierung ist nicht erlaubt + DeviceAuth: + NotExisting: Benutzercode existiert nicht optional: (optional) diff --git a/internal/api/ui/login/static/i18n/en.yaml b/internal/api/ui/login/static/i18n/en.yaml index 14b0d90011..14ac6a5808 100644 --- a/internal/api/ui/login/static/i18n/en.yaml +++ b/internal/api/ui/login/static/i18n/en.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: Device Authorization + UserCode: + Label: User Code + Description: Enter the user code presented on the device. + ButtonNext: next + Action: + Description: Grant device access. + GrantDevice: you are about to grant device + AccessToScopes: access to the following scopes + Button: + Allow: allow + Deny: deny + Done: + Description: Done. + Approved: Device authorization approved. You can now return to the device. + Denied: Device authorization denied. You can now return to the device. + Footer: PoweredBy: Powered By Tos: TOS @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: Registration is not allowed + DeviceAuth: + NotExisting: User Code doesn't exist optional: (optional) diff --git a/internal/api/ui/login/static/i18n/fr.yaml b/internal/api/ui/login/static/i18n/fr.yaml index be50b4327e..3678b555a0 100644 --- a/internal/api/ui/login/static/i18n/fr.yaml +++ b/internal/api/ui/login/static/i18n/fr.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: Autorisation de l'appareil + UserCode: + Label: Code d'utilisateur + Description: Saisissez le code utilisateur présenté sur l'appareil. + ButtonNext: suivant + Action: + Description: Accordez l'accès à l'appareil. + GrantDevice: vous êtes sur le point d'accorder un appareil + AccessToScopes: accès aux périmètres suivants + Button: + Allow: permettre + Deny: refuser + Done: + Description: Fait. + Approved: Autorisation de l'appareil approuvée. Vous pouvez maintenant retourner à l'appareil. + Denied: Autorisation de l'appareil refusée. Vous pouvez maintenant retourner à l'appareil. + Footer: PoweredBy: Promulgué par Tos: TOS @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: L'enregistrement n'est pas autorisé + DeviceAuth: + NotExisting: Le code utilisateur n'existe pas optional: (facultatif) diff --git a/internal/api/ui/login/static/i18n/it.yaml b/internal/api/ui/login/static/i18n/it.yaml index b27e27b072..61bf0e3190 100644 --- a/internal/api/ui/login/static/i18n/it.yaml +++ b/internal/api/ui/login/static/i18n/it.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: Autorizzazione del dispositivo + UserCode: + Label: Codice utente + Description: Inserire il codice utente presentato sul dispositivo. + ButtonNext: prossimo + Action: + Description: Concedi l'accesso al dispositivo. + GrantDevice: stai per concedere il dispositivo + AccessToScopes: accesso ai seguenti ambiti + Button: + Allow: permettere + Deny: negare + Done: + Description: Fatto. + Approved: Autorizzazione del dispositivo approvata. Ora puoi tornare al dispositivo. + Denied: Autorizzazione dispositivo negata. Ora puoi tornare al dispositivo. + Footer: PoweredBy: Alimentato da Tos: Termini di servizio @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: la registrazione non è consentita. + DeviceAuth: + NotExisting: Il codice utente non esiste optional: (opzionale) diff --git a/internal/api/ui/login/static/i18n/ja.yaml b/internal/api/ui/login/static/i18n/ja.yaml index bdb9ec09d7..3a7b964b2b 100644 --- a/internal/api/ui/login/static/i18n/ja.yaml +++ b/internal/api/ui/login/static/i18n/ja.yaml @@ -309,6 +309,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: デバイス認証 + UserCode: + Label: ユーザーコード + Description: デバイスに表示されたユーザー コードを入力します。 + ButtonNext: 次 + Action: + Description: デバイスへのアクセスを許可します。 + GrantDevice: デバイスを許可しようとしています + AccessToScopes: 次のスコープへのアクセス + Button: + Allow: 許可する + Deny: 拒否 + Done: + Description: 終わり。 + Approved: デバイス認証が承認されました。 これで、デバイスに戻ることができます。 + Denied: デバイス認証が拒否されました。 これで、デバイスに戻ることができます。 + Footer: PoweredBy: Powered By Tos: TOS @@ -385,5 +403,7 @@ Errors: IAM: LockoutPolicy: NotExisting: ロックアウトポリシーが存在しません + DeviceAuth: + NotExisting: ユーザーコードが存在しません optional: "(オプション)" diff --git a/internal/api/ui/login/static/i18n/pl.yaml b/internal/api/ui/login/static/i18n/pl.yaml index ca038b2561..894de884bd 100644 --- a/internal/api/ui/login/static/i18n/pl.yaml +++ b/internal/api/ui/login/static/i18n/pl.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: Autoryzacja urządzenia + UserCode: + Label: Kod użytkownika + Description: Wprowadź kod użytkownika prezentowany na urządzeniu. + ButtonNext: Następny + Action: + Description: Przyznaj dostęp do urządzenia. + GrantDevice: zamierzasz przyznać urządzenie + AccessToScopes: dostęp do następujących zakresów + Button: + Allow: umożliwić + Deny: zaprzeczyć + Done: + Description: Zrobione. + Approved: Zatwierdzono autoryzację urządzenia. Możesz teraz wrócić do urządzenia. + Denied: Odmowa autoryzacji urządzenia. Możesz teraz wrócić do urządzenia. + Footer: PoweredBy: Obsługiwane przez Tos: TOS @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: Rejestracja nie jest dozwolona + DeviceAuth: + NotExisting: Kod użytkownika nie istnieje optional: (opcjonalny) diff --git a/internal/api/ui/login/static/i18n/zh.yaml b/internal/api/ui/login/static/i18n/zh.yaml index 830476299f..e2947e6d17 100644 --- a/internal/api/ui/login/static/i18n/zh.yaml +++ b/internal/api/ui/login/static/i18n/zh.yaml @@ -317,6 +317,24 @@ ExternalNotFound: Japanese: 日本語 Spanish: Español +DeviceAuth: + Title: 设备授权 + UserCode: + Label: 用户代码 + Description: 输入设备上显示的用户代码。 + ButtonNext: 下一个 + Action: + Description: 授予设备访问权限。 + GrantDevice: 您即将授予设备 + AccessToScopes: 访问以下范围 + Button: + Allow: 允许 + Deny: 否定 + Done: + Description: 完毕。 + Approved: 设备授权已批准。 您现在可以返回设备。 + Denied: 设备授权被拒绝。 您现在可以返回设备。 + Footer: PoweredBy: Powered By Tos: 服务条款 @@ -425,5 +443,7 @@ Errors: Org: LoginPolicy: RegistrationNotAllowed: 不允许注册 + DeviceAuth: + NotExisting: 用户代码不存在 optional: (可选) diff --git a/internal/api/ui/login/static/templates/device_action.html b/internal/api/ui/login/static/templates/device_action.html new file mode 100644 index 0000000000..4e0cc2801d --- /dev/null +++ b/internal/api/ui/login/static/templates/device_action.html @@ -0,0 +1,18 @@ +{{template "main-top" .}} + +

{{.Title}}

+

+ {{.Username}}, {{t "DeviceAuth.Action.GrantDevice"}} {{.ClientID}} {{t "DeviceAuth.Action.AccessToScopes"}}: {{.Scopes}}. +

+
+ {{ .CSRF }} + + + +
+ +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/device_usercode.html b/internal/api/ui/login/static/templates/device_usercode.html new file mode 100644 index 0000000000..5d053cabac --- /dev/null +++ b/internal/api/ui/login/static/templates/device_usercode.html @@ -0,0 +1,21 @@ +{{template "main-top" .}} + +

{{.Title}}

+
+ + {{ .CSRF }} + +
+ + +
+ + {{template "error-message" .}} + +
+ + +
+
+ +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/static/templates/success.html b/internal/api/ui/login/static/templates/success.html new file mode 100644 index 0000000000..bc5042f13b --- /dev/null +++ b/internal/api/ui/login/static/templates/success.html @@ -0,0 +1,12 @@ +{{template "main-top" .}} + +
+
+ +

+ {{ .Message }} +

+
+
+ +{{template "main-bottom" .}} diff --git a/internal/api/ui/login/statik/generate.go b/internal/api/ui/login/statik/generate.go index 75330afad9..5388980631 100644 --- a/internal/api/ui/login/statik/generate.go +++ b/internal/api/ui/login/statik/generate.go @@ -1,3 +1,3 @@ package statik -//go:generate statik -src=../static -dest=.. -ns=login +//go:generate statik -f -src=../static -dest=.. -ns=login diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request.go b/internal/auth/repository/eventsourcing/eventstore/auth_request.go index 34a30a6abf..fcabd8f77f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request.go @@ -23,6 +23,7 @@ import ( "github.com/zitadel/zitadel/internal/telemetry/tracing" user_model "github.com/zitadel/zitadel/internal/user/model" user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" + "github.com/zitadel/zitadel/internal/view/repository" ) const unknownUserID = "UNKNOWN" @@ -64,7 +65,9 @@ type privacyPolicyProvider interface { type userSessionViewProvider interface { UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) UserSessionsByAgentID(string, string) ([]*user_view_model.UserSessionView, error) + GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) } + type userViewProvider interface { UserByID(string, string) (*user_view_model.UserView, error) } @@ -654,7 +657,7 @@ func (repo *AuthRequestRepo) checkLoginName(ctx context.Context, request *domain preferredLoginName += "@" + request.RequestedPrimaryDomain } } - user, err = repo.checkLoginNameInputForResourceOwner(request, preferredLoginName) + user, err = repo.checkLoginNameInputForResourceOwner(ctx, request, preferredLoginName) } else { user, err = repo.checkLoginNameInput(ctx, request, preferredLoginName) } @@ -729,12 +732,12 @@ func (repo *AuthRequestRepo) checkDomainDiscovery(ctx context.Context, request * func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { // always check the loginname first - user, err := repo.View.UserByLoginName(loginNameInput, request.InstanceID) + user, err := repo.View.UserByLoginName(ctx, loginNameInput, request.InstanceID) if err == nil { // and take the user regardless if there would be a user with that email or phone return user, repo.checkLoginPolicyWithResourceOwner(ctx, request, user.ResourceOwner) } - user, emailErr := repo.View.UserByEmail(loginNameInput, request.InstanceID) + user, emailErr := repo.View.UserByEmail(ctx, loginNameInput, request.InstanceID) if emailErr == nil { // if there was a single user with the specified email // load and check the login policy @@ -747,7 +750,7 @@ func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *d return user, nil } } - user, phoneErr := repo.View.UserByPhone(loginNameInput, request.InstanceID) + user, phoneErr := repo.View.UserByPhone(ctx, loginNameInput, request.InstanceID) if phoneErr == nil { // if there was a single user with the specified phone // load and check the login policy @@ -765,9 +768,9 @@ func (repo *AuthRequestRepo) checkLoginNameInput(ctx context.Context, request *d return nil, err } -func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { +func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(ctx context.Context, request *domain.AuthRequest, loginNameInput string) (*user_view_model.UserView, error) { // always check the loginname first - user, err := repo.View.UserByLoginNameAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + user, err := repo.View.UserByLoginNameAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID) if err == nil { // and take the user regardless if there would be a user with that email or phone return user, nil @@ -775,7 +778,7 @@ func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithEmail { // if login by email is allowed and there was a single user with the specified email // take that user (and ignore possible phone number matches) - user, emailErr := repo.View.UserByEmailAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + user, emailErr := repo.View.UserByEmailAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID) if emailErr == nil { return user, nil } @@ -783,7 +786,7 @@ func (repo *AuthRequestRepo) checkLoginNameInputForResourceOwner(request *domain if request.LoginPolicy != nil && !request.LoginPolicy.DisableLoginWithPhone { // if login by phone is allowed and there was a single user with the specified phone // take that user - user, phoneErr := repo.View.UserByPhoneAndResourceOwner(loginNameInput, request.RequestedOrgID, request.InstanceID) + user, phoneErr := repo.View.UserByPhoneAndResourceOwner(ctx, loginNameInput, request.RequestedOrgID, request.InstanceID) if phoneErr == nil { return user, nil } @@ -1298,12 +1301,20 @@ func userSessionsByUserAgentID(provider userSessionViewProvider, agentID, instan } func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eventProvider userEventProvider, agentID string, user *user_model.UserView) (*user_model.UserSessionView, error) { - session, err := provider.UserSessionByIDs(agentID, user.ID, authz.GetInstance(ctx).InstanceID()) + instanceID := authz.GetInstance(ctx).InstanceID() + session, err := provider.UserSessionByIDs(agentID, user.ID, instanceID) if err != nil { if !errors.IsNotFound(err) { return nil, err } + sequence, err := provider.GetLatestUserSessionSequence(ctx, instanceID) + logging.WithFields("instanceID", instanceID, "userID", user.ID). + OnError(err). + Errorf("could not get current sequence for userSessionByIDs") session = &user_view_model.UserSessionView{UserAgentID: agentID, UserID: user.ID} + if sequence != nil { + session.Sequence = sequence.CurrentSequence + } } events, err := eventProvider.UserEventsByID(ctx, user.ID, session.Sequence) if err != nil { @@ -1446,7 +1457,7 @@ func linkingIDPConfigExistingInAllowedIDPs(linkingUsers []*domain.ExternalUser, func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *user_model.UserView, userGrantProvider userGrantProvider) (_ bool, err error) { var project *query.Project switch request.Request.Type() { - case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML: + case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML, domain.AuthRequestTypeDevice: project, err = userGrantProvider.ProjectByClientID(ctx, request.ApplicationID, false) if err != nil { return false, err @@ -1467,13 +1478,13 @@ func userGrantRequired(ctx context.Context, request *domain.AuthRequest, user *u func projectRequired(ctx context.Context, request *domain.AuthRequest, projectProvider projectProvider) (missingGrant bool, err error) { var project *query.Project switch request.Request.Type() { - case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML: + case domain.AuthRequestTypeOIDC, domain.AuthRequestTypeSAML, domain.AuthRequestTypeDevice: project, err = projectProvider.ProjectByClientID(ctx, request.ApplicationID, false) if err != nil { return false, err } default: - return false, errors.ThrowPreconditionFailed(nil, "EVENT-dfrw2", "Errors.AuthRequest.RequestTypeNotSupported") + return false, errors.ThrowPreconditionFailed(nil, "EVENT-ku4He", "Errors.AuthRequest.RequestTypeNotSupported") } // if the user and project are part of the same organisation we do not need to check if the project exists on that org if !project.HasProjectCheck || project.ResourceOwner == request.UserOrgID { diff --git a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go index 04d9e06646..c5dd8f411f 100644 --- a/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go +++ b/internal/auth/repository/eventsourcing/eventstore/auth_request_test.go @@ -19,6 +19,7 @@ import ( user_model "github.com/zitadel/zitadel/internal/user/model" user_es_model "github.com/zitadel/zitadel/internal/user/repository/eventsourcing/model" user_view_model "github.com/zitadel/zitadel/internal/user/repository/view/model" + "github.com/zitadel/zitadel/internal/view/repository" ) var ( @@ -35,6 +36,10 @@ func (m *mockViewNoUserSession) UserSessionsByAgentID(string, string) ([]*user_v return nil, nil } +func (m *mockViewNoUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return &repository.CurrentSequence{}, nil +} + type mockViewErrUserSession struct{} func (m *mockViewErrUserSession) UserSessionByIDs(string, string, string) (*user_view_model.UserSessionView, error) { @@ -45,6 +50,10 @@ func (m *mockViewErrUserSession) UserSessionsByAgentID(string, string) ([]*user_ return nil, errors.ThrowInternal(nil, "id", "internal error") } +func (m *mockViewErrUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return &repository.CurrentSequence{}, nil +} + type mockViewUserSession struct { ExternalLoginVerification time.Time PasswordlessVerification time.Time @@ -82,6 +91,10 @@ func (m *mockViewUserSession) UserSessionsByAgentID(string, string) ([]*user_vie return sessions, nil } +func (m *mockViewUserSession) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return &repository.CurrentSequence{}, nil +} + type mockViewNoUser struct{} func (m *mockViewNoUser) UserByID(string, string) (*user_view_model.UserView, error) { diff --git a/internal/auth/repository/eventsourcing/eventstore/refresh_token.go b/internal/auth/repository/eventsourcing/eventstore/refresh_token.go index 33ea1d868c..9edc5af154 100644 --- a/internal/auth/repository/eventsourcing/eventstore/refresh_token.go +++ b/internal/auth/repository/eventsourcing/eventstore/refresh_token.go @@ -42,15 +42,24 @@ func (r *RefreshTokenRepo) RefreshTokenByToken(ctx context.Context, refreshToken } func (r *RefreshTokenRepo) RefreshTokenByID(ctx context.Context, tokenID, userID string) (*usr_model.RefreshTokenView, error) { - tokenView, viewErr := r.View.RefreshTokenByID(tokenID, authz.GetInstance(ctx).InstanceID()) + instanceID := authz.GetInstance(ctx).InstanceID() + tokenView, viewErr := r.View.RefreshTokenByID(tokenID, instanceID) if viewErr != nil && !errors.IsNotFound(viewErr) { return nil, viewErr } if errors.IsNotFound(viewErr) { + sequence, err := r.View.GetLatestRefreshTokenSequence(ctx, instanceID) + logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID). + OnError(err). + Errorf("could not get current sequence for RefreshTokenByID") + tokenView = new(model.RefreshTokenView) tokenView.ID = tokenID tokenView.UserID = userID - tokenView.InstanceID = authz.GetInstance(ctx).InstanceID() + tokenView.InstanceID = instanceID + if sequence != nil { + tokenView.Sequence = sequence.CurrentSequence + } } events, esErr := r.getUserEvents(ctx, userID, tokenView.InstanceID, tokenView.Sequence) @@ -80,7 +89,7 @@ func (r *RefreshTokenRepo) SearchMyRefreshTokens(ctx context.Context, userID str if err != nil { return nil, err } - sequence, err := r.View.GetLatestRefreshTokenSequence(authz.GetInstance(ctx).InstanceID()) + sequence, err := r.View.GetLatestRefreshTokenSequence(ctx, authz.GetInstance(ctx).InstanceID()) logging.Log("EVENT-GBdn4").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Warn("could not read latest refresh token sequence") request.Queries = append(request.Queries, &usr_model.RefreshTokenSearchQuery{Key: usr_model.RefreshTokenSearchKeyUserID, Method: domain.SearchMethodEquals, Value: userID}) tokens, count, err := r.View.SearchRefreshTokens(request) diff --git a/internal/auth/repository/eventsourcing/eventstore/token.go b/internal/auth/repository/eventsourcing/eventstore/token.go index ec982ea3b2..7894a6f4cc 100644 --- a/internal/auth/repository/eventsourcing/eventstore/token.go +++ b/internal/auth/repository/eventsourcing/eventstore/token.go @@ -34,15 +34,25 @@ func (repo *TokenRepo) IsTokenValid(ctx context.Context, userID, tokenID string) } func (repo *TokenRepo) TokenByIDs(ctx context.Context, userID, tokenID string) (*usr_model.TokenView, error) { - token, viewErr := repo.View.TokenByIDs(tokenID, userID, authz.GetInstance(ctx).InstanceID()) + instanceID := authz.GetInstance(ctx).InstanceID() + + token, viewErr := repo.View.TokenByIDs(tokenID, userID, instanceID) if viewErr != nil && !errors.IsNotFound(viewErr) { return nil, viewErr } if errors.IsNotFound(viewErr) { + sequence, err := repo.View.GetLatestTokenSequence(ctx, instanceID) + logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID). + OnError(err). + Errorf("could not get current sequence for TokenByIDs") + token = new(model.TokenView) token.ID = tokenID token.UserID = userID - token.InstanceID = authz.GetInstance(ctx).InstanceID() + token.InstanceID = instanceID + if sequence != nil { + token.Sequence = sequence.CurrentSequence + } } events, esErr := repo.getUserEvents(ctx, userID, token.InstanceID, token.Sequence) diff --git a/internal/auth/repository/eventsourcing/handler/refresh_token.go b/internal/auth/repository/eventsourcing/handler/refresh_token.go index 29e228caea..51f158b681 100644 --- a/internal/auth/repository/eventsourcing/handler/refresh_token.go +++ b/internal/auth/repository/eventsourcing/handler/refresh_token.go @@ -62,16 +62,16 @@ func (t *RefreshToken) AggregateTypes() []es_models.AggregateType { return []es_models.AggregateType{user.AggregateType, project.AggregateType, instance.AggregateType} } -func (t *RefreshToken) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := t.view.GetLatestRefreshTokenSequence(instanceID) +func (t *RefreshToken) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { + sequence, err := t.view.GetLatestRefreshTokenSequence(ctx, instanceID) if err != nil { return 0, err } return sequence.CurrentSequence, nil } -func (t *RefreshToken) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) { - sequences, err := t.view.GetLatestRefreshTokenSequences(instanceIDs) +func (t *RefreshToken) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) { + sequences, err := t.view.GetLatestRefreshTokenSequences(ctx, instanceIDs) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/handler/token.go b/internal/auth/repository/eventsourcing/handler/token.go index 280c85b6b4..7bb5bd321b 100644 --- a/internal/auth/repository/eventsourcing/handler/token.go +++ b/internal/auth/repository/eventsourcing/handler/token.go @@ -67,16 +67,16 @@ func (_ *Token) AggregateTypes() []es_models.AggregateType { return []es_models.AggregateType{user.AggregateType, project.AggregateType, instance.AggregateType} } -func (t *Token) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := t.view.GetLatestTokenSequence(instanceID) +func (t *Token) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { + sequence, err := t.view.GetLatestTokenSequence(ctx, instanceID) if err != nil { return 0, err } return sequence.CurrentSequence, nil } -func (t *Token) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) { - sequences, err := t.view.GetLatestTokenSequences(instanceIDs) +func (t *Token) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) { + sequences, err := t.view.GetLatestTokenSequences(ctx, instanceIDs) if err != nil { return nil, err } @@ -145,11 +145,13 @@ func (t *Token) Reduce(event *es_models.Event) (err error) { if err != nil { return err } - applicationsIDs := make([]string, 0, len(project.Applications)) + clientIDs := make([]string, 0, len(project.Applications)) for _, app := range project.Applications { - applicationsIDs = append(applicationsIDs, app.AppID) + if app.OIDCConfig != nil { + clientIDs = append(clientIDs, app.OIDCConfig.ClientID) + } } - return t.view.DeleteApplicationTokens(event, applicationsIDs...) + return t.view.DeleteApplicationTokens(event, clientIDs...) case instance.InstanceRemovedEventType: return t.view.DeleteInstanceTokens(event) case org.OrgRemovedEventType: @@ -208,7 +210,7 @@ func (t *Token) OnSuccess(instanceIDs []string) error { } func (t *Token) getProjectByID(ctx context.Context, projID, instanceID string) (*proj_model.Project, error) { - query, err := proj_view.ProjectByIDQuery(projID, instanceID, 0) + projectQuery, err := proj_view.ProjectByIDQuery(projID, instanceID, 0) if err != nil { return nil, err } @@ -217,7 +219,7 @@ func (t *Token) getProjectByID(ctx context.Context, projID, instanceID string) ( AggregateID: projID, }, } - err = es_sdk.Filter(ctx, t.Eventstore().FilterEvents, esProject.AppendEvents, query) + err = es_sdk.Filter(ctx, t.Eventstore().FilterEvents, esProject.AppendEvents, projectQuery) if err != nil && !caos_errs.IsNotFound(err) { return nil, err } diff --git a/internal/auth/repository/eventsourcing/handler/user.go b/internal/auth/repository/eventsourcing/handler/user.go index d19d09281a..b226f54b13 100644 --- a/internal/auth/repository/eventsourcing/handler/user.go +++ b/internal/auth/repository/eventsourcing/handler/user.go @@ -68,16 +68,16 @@ func (_ *User) AggregateTypes() []es_models.AggregateType { return []es_models.AggregateType{user_repo.AggregateType, org.AggregateType, instance.AggregateType} } -func (u *User) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := u.view.GetLatestUserSequence(instanceID) +func (u *User) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { + sequence, err := u.view.GetLatestUserSequence(ctx, instanceID) if err != nil { return 0, err } return sequence.CurrentSequence, nil } -func (u *User) EventQuery(instanceIDs []string) (*es_models.SearchQuery, error) { - sequences, err := u.view.GetLatestUserSequences(instanceIDs) +func (u *User) EventQuery(ctx context.Context, instanceIDs []string) (*es_models.SearchQuery, error) { + sequences, err := u.view.GetLatestUserSequences(ctx, instanceIDs) if err != nil { return nil, err } @@ -158,6 +158,11 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) { if !errors.IsNotFound(err) { return err } + logging.WithFields( + "instance", event.InstanceID, + "userID", event.AggregateID, + "eventType", event.Type, + ).Info("user not found in view") query, err := usr_view.UserByIDQuery(event.AggregateID, event.InstanceID, 0) if err != nil { return err @@ -181,6 +186,11 @@ func (u *User) ProcessUser(event *es_models.Event) (err error) { if !errors.IsNotFound(err) { return err } + logging.WithFields( + "instance", event.InstanceID, + "userID", event.AggregateID, + "eventType", event.Type, + ).Info("user not found in view") query, err := usr_view.UserByIDQuery(event.AggregateID, event.InstanceID, 0) if err != nil { return err @@ -291,7 +301,7 @@ func (u *User) OnSuccess(instanceIDs []string) error { } func (u *User) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) { - query, err := view.OrgByIDQuery(orgID, instanceID, 0) + orgQuery, err := view.OrgByIDQuery(orgID, instanceID, 0) if err != nil { return nil, err } @@ -301,7 +311,7 @@ func (u *User) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_m AggregateID: orgID, }, } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query) + err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, orgQuery) if err != nil && !errors.IsNotFound(err) { return nil, err } diff --git a/internal/auth/repository/eventsourcing/handler/user_session.go b/internal/auth/repository/eventsourcing/handler/user_session.go index a76e478512..a0148edd68 100644 --- a/internal/auth/repository/eventsourcing/handler/user_session.go +++ b/internal/auth/repository/eventsourcing/handler/user_session.go @@ -65,16 +65,16 @@ func (_ *UserSession) AggregateTypes() []models.AggregateType { return []models.AggregateType{user.AggregateType, org.AggregateType, instance.AggregateType} } -func (u *UserSession) CurrentSequence(instanceID string) (uint64, error) { - sequence, err := u.view.GetLatestUserSessionSequence(instanceID) +func (u *UserSession) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { + sequence, err := u.view.GetLatestUserSessionSequence(ctx, instanceID) if err != nil { return 0, err } return sequence.CurrentSequence, nil } -func (u *UserSession) EventQuery(instanceIDs []string) (*models.SearchQuery, error) { - sequences, err := u.view.GetLatestUserSessionSequences(instanceIDs) +func (u *UserSession) EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) { + sequences, err := u.view.GetLatestUserSessionSequences(ctx, instanceIDs) if err != nil { return nil, err } @@ -231,7 +231,7 @@ func (u *UserSession) loginNameInformation(ctx context.Context, orgID string, in } func (u *UserSession) getOrgByID(ctx context.Context, orgID, instanceID string) (*org_model.Org, error) { - query, err := view.OrgByIDQuery(orgID, instanceID, 0) + orgQuery, err := view.OrgByIDQuery(orgID, instanceID, 0) if err != nil { return nil, err } @@ -241,7 +241,7 @@ func (u *UserSession) getOrgByID(ctx context.Context, orgID, instanceID string) AggregateID: orgID, }, } - err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, query) + err = es_sdk.Filter(ctx, u.Eventstore().FilterEvents, esOrg.AppendEvents, orgQuery) if err != nil && !errors.IsNotFound(err) { return nil, err } diff --git a/internal/auth/repository/eventsourcing/repository.go b/internal/auth/repository/eventsourcing/repository.go index 852ebcca91..454daffd11 100644 --- a/internal/auth/repository/eventsourcing/repository.go +++ b/internal/auth/repository/eventsourcing/repository.go @@ -34,8 +34,8 @@ type EsRepository struct { eventstore.OrgRepository } -func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.EncryptionAlgorithm, userEncryption crypto.EncryptionAlgorithm) (*EsRepository, error) { - es, err := v1.Start(dbClient) +func Start(ctx context.Context, conf Config, systemDefaults sd.SystemDefaults, command *command.Commands, queries *query.Queries, dbClient *database.DB, esV2 *eventstore2.Eventstore, oidcEncryption crypto.EncryptionAlgorithm, userEncryption crypto.EncryptionAlgorithm, allowOrderByCreationDate bool) (*EsRepository, error) { + es, err := v1.Start(dbClient, allowOrderByCreationDate) if err != nil { return nil, err } diff --git a/internal/auth/repository/eventsourcing/view/refresh_token.go b/internal/auth/repository/eventsourcing/view/refresh_token.go index 8c1342f6f8..2740c2ec84 100644 --- a/internal/auth/repository/eventsourcing/view/refresh_token.go +++ b/internal/auth/repository/eventsourcing/view/refresh_token.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" user_model "github.com/zitadel/zitadel/internal/user/model" @@ -81,12 +83,12 @@ func (v *View) DeleteOrgRefreshTokens(event *models.Event) error { return v.ProcessedRefreshTokenSequence(event) } -func (v *View) GetLatestRefreshTokenSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(refreshTokenTable, instanceID) +func (v *View) GetLatestRefreshTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return v.latestSequence(ctx, refreshTokenTable, instanceID) } -func (v *View) GetLatestRefreshTokenSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) { - return v.latestSequences(refreshTokenTable, instanceIDs) +func (v *View) GetLatestRefreshTokenSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return v.latestSequences(ctx, refreshTokenTable, instanceIDs) } func (v *View) ProcessedRefreshTokenSequence(event *models.Event) error { diff --git a/internal/auth/repository/eventsourcing/view/sequence.go b/internal/auth/repository/eventsourcing/view/sequence.go index c1e3b0b4e2..71c77cb6a4 100644 --- a/internal/auth/repository/eventsourcing/view/sequence.go +++ b/internal/auth/repository/eventsourcing/view/sequence.go @@ -1,6 +1,7 @@ package view import ( + "context" "time" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -15,12 +16,12 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error { return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate) } -func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) { - return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID) +func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) { + return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID) } -func (v *View) latestSequences(viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) { - return repository.LatestSequences(v.Db, sequencesTable, viewName, instanceIDs) +func (v *View) latestSequences(ctx context.Context, viewName string, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return repository.LatestSequences(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceIDs) } func (v *View) updateSpoolerRunSequence(viewName string, instanceIDs []string) error { diff --git a/internal/auth/repository/eventsourcing/view/token.go b/internal/auth/repository/eventsourcing/view/token.go index 132c3ecd94..3f3909cdb3 100644 --- a/internal/auth/repository/eventsourcing/view/token.go +++ b/internal/auth/repository/eventsourcing/view/token.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" usr_view "github.com/zitadel/zitadel/internal/user/repository/view" @@ -92,12 +94,12 @@ func (v *View) DeleteOrgTokens(event *models.Event) error { return v.ProcessedTokenSequence(event) } -func (v *View) GetLatestTokenSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(tokenTable, instanceID) +func (v *View) GetLatestTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return v.latestSequence(ctx, tokenTable, instanceID) } -func (v *View) GetLatestTokenSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) { - return v.latestSequences(tokenTable, instanceIDs) +func (v *View) GetLatestTokenSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return v.latestSequences(ctx, tokenTable, instanceIDs) } func (v *View) ProcessedTokenSequence(event *models.Event) error { diff --git a/internal/auth/repository/eventsourcing/view/user.go b/internal/auth/repository/eventsourcing/view/user.go index 51cd52f007..6a82f6ed63 100644 --- a/internal/auth/repository/eventsourcing/view/user.go +++ b/internal/auth/repository/eventsourcing/view/user.go @@ -3,7 +3,8 @@ package view import ( "context" - "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/query" @@ -21,16 +22,16 @@ func (v *View) UserByID(userID, instanceID string) (*model.UserView, error) { return view.UserByID(v.Db, userTable, userID, instanceID) } -func (v *View) UserByLoginName(loginName, instanceID string) (*model.UserView, error) { +func (v *View) UserByLoginName(ctx context.Context, loginName, instanceID string) (*model.UserView, error) { loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName) if err != nil { return nil, err } - return v.userByID(instanceID, loginNameQuery) + return v.userByID(ctx, instanceID, loginNameQuery) } -func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner, instanceID string) (*model.UserView, error) { +func (v *View) UserByLoginNameAndResourceOwner(ctx context.Context, loginName, resourceOwner, instanceID string) (*model.UserView, error) { loginNameQuery, err := query.NewUserLoginNamesSearchQuery(loginName) if err != nil { return nil, err @@ -40,18 +41,18 @@ func (v *View) UserByLoginNameAndResourceOwner(loginName, resourceOwner, instanc return nil, err } - return v.userByID(instanceID, loginNameQuery, resourceOwnerQuery) + return v.userByID(ctx, instanceID, loginNameQuery, resourceOwnerQuery) } -func (v *View) UserByEmail(email, instanceID string) (*model.UserView, error) { +func (v *View) UserByEmail(ctx context.Context, email, instanceID string) (*model.UserView, error) { emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEqualsIgnoreCase) if err != nil { return nil, err } - return v.userByID(instanceID, emailQuery) + return v.userByID(ctx, instanceID, emailQuery) } -func (v *View) UserByEmailAndResourceOwner(email, resourceOwner, instanceID string) (*model.UserView, error) { +func (v *View) UserByEmailAndResourceOwner(ctx context.Context, email, resourceOwner, instanceID string) (*model.UserView, error) { emailQuery, err := query.NewUserVerifiedEmailSearchQuery(email, query.TextEquals) if err != nil { return nil, err @@ -61,18 +62,18 @@ func (v *View) UserByEmailAndResourceOwner(email, resourceOwner, instanceID stri return nil, err } - return v.userByID(instanceID, emailQuery, resourceOwnerQuery) + return v.userByID(ctx, instanceID, emailQuery, resourceOwnerQuery) } -func (v *View) UserByPhone(phone, instanceID string) (*model.UserView, error) { +func (v *View) UserByPhone(ctx context.Context, phone, instanceID string) (*model.UserView, error) { phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals) if err != nil { return nil, err } - return v.userByID(instanceID, phoneQuery) + return v.userByID(ctx, instanceID, phoneQuery) } -func (v *View) UserByPhoneAndResourceOwner(phone, resourceOwner, instanceID string) (*model.UserView, error) { +func (v *View) UserByPhoneAndResourceOwner(ctx context.Context, phone, resourceOwner, instanceID string) (*model.UserView, error) { phoneQuery, err := query.NewUserVerifiedPhoneSearchQuery(phone, query.TextEquals) if err != nil { return nil, err @@ -82,12 +83,10 @@ func (v *View) UserByPhoneAndResourceOwner(phone, resourceOwner, instanceID stri return nil, err } - return v.userByID(instanceID, phoneQuery, resourceOwnerQuery) + return v.userByID(ctx, instanceID, phoneQuery, resourceOwnerQuery) } -func (v *View) userByID(instanceID string, queries ...query.SearchQuery) (*model.UserView, error) { - ctx := authz.WithInstanceID(context.Background(), instanceID) - +func (v *View) userByID(ctx context.Context, instanceID string, queries ...query.SearchQuery) (*model.UserView, error) { queriedUser, err := v.query.GetNotifyUser(ctx, true, false, queries...) if err != nil { return nil, err @@ -99,7 +98,14 @@ func (v *View) userByID(instanceID string, queries ...query.SearchQuery) (*model } if err != nil { + sequence, err := v.GetLatestUserSequence(ctx, instanceID) + logging.WithFields("instanceID", instanceID). + OnError(err). + Errorf("could not get current sequence for userByID") user = new(model.UserView) + if sequence != nil { + user.Sequence = sequence.CurrentSequence + } } query, err := view.UserByIDQuery(queriedUser.ID, instanceID, user.Sequence) @@ -188,12 +194,12 @@ func (v *View) UpdateOrgOwnerRemovedUsers(event *models.Event) error { return v.ProcessedUserSequence(event) } -func (v *View) GetLatestUserSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(userTable, instanceID) +func (v *View) GetLatestUserSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return v.latestSequence(ctx, userTable, instanceID) } -func (v *View) GetLatestUserSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) { - return v.latestSequences(userTable, instanceIDs) +func (v *View) GetLatestUserSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return v.latestSequences(ctx, userTable, instanceIDs) } func (v *View) ProcessedUserSequence(event *models.Event) error { diff --git a/internal/auth/repository/eventsourcing/view/user_session.go b/internal/auth/repository/eventsourcing/view/user_session.go index 52ac559384..4e3803e77b 100644 --- a/internal/auth/repository/eventsourcing/view/user_session.go +++ b/internal/auth/repository/eventsourcing/view/user_session.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/user/repository/view" @@ -72,12 +74,12 @@ func (v *View) DeleteOrgUserSessions(event *models.Event) error { return v.ProcessedUserSessionSequence(event) } -func (v *View) GetLatestUserSessionSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(userSessionTable, instanceID) +func (v *View) GetLatestUserSessionSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return v.latestSequence(ctx, userSessionTable, instanceID) } -func (v *View) GetLatestUserSessionSequences(instanceIDs []string) ([]*repository.CurrentSequence, error) { - return v.latestSequences(userSessionTable, instanceIDs) +func (v *View) GetLatestUserSessionSequences(ctx context.Context, instanceIDs []string) ([]*repository.CurrentSequence, error) { + return v.latestSequences(ctx, userSessionTable, instanceIDs) } func (v *View) ProcessedUserSessionSequence(event *models.Event) error { diff --git a/internal/auth/repository/eventsourcing/view/view.go b/internal/auth/repository/eventsourcing/view/view.go index 80f683bc80..b65badf1e5 100644 --- a/internal/auth/repository/eventsourcing/view/view.go +++ b/internal/auth/repository/eventsourcing/view/view.go @@ -1,8 +1,11 @@ package view import ( + "context" + "github.com/jinzhu/gorm" + "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/database" eventstore "github.com/zitadel/zitadel/internal/eventstore/v1" @@ -16,6 +19,7 @@ type View struct { idGenerator id.Generator query *query.Queries es eventstore.Eventstore + client *database.DB } func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, queries *query.Queries, idGenerator id.Generator, es eventstore.Eventstore) (*View, error) { @@ -29,9 +33,14 @@ func StartView(sqlClient *database.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGenerator: idGenerator, query: queries, es: es, + client: sqlClient, }, nil } func (v *View) Health() (err error) { return v.Db.DB().Ping() } + +func (v *View) TimeTravel(ctx context.Context, tableName string) string { + return tableName + v.client.Timetravel(call.Took(ctx)) +} diff --git a/internal/authz/authz.go b/internal/authz/authz.go index f8f21e4125..6106d8a4e5 100644 --- a/internal/authz/authz.go +++ b/internal/authz/authz.go @@ -8,6 +8,6 @@ import ( "github.com/zitadel/zitadel/internal/query" ) -func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) { - return eventsourcing.Start(queries, dbClient, keyEncryptionAlgorithm, externalSecure) +func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure, allowOrderByCreationDate bool) (repository.Repository, error) { + return eventsourcing.Start(queries, dbClient, keyEncryptionAlgorithm, externalSecure, allowOrderByCreationDate) } diff --git a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go index 4ef653d207..b2bff61557 100644 --- a/internal/authz/repository/eventsourcing/eventstore/token_verifier.go +++ b/internal/authz/repository/eventsourcing/eventstore/token_verifier.go @@ -44,16 +44,16 @@ func (repo *TokenVerifierRepo) tokenByID(ctx context.Context, tokenID, userID st defer func() { span.EndWithError(err) }() instanceID := authz.GetInstance(ctx).InstanceID() - sequence, err := repo.View.GetLatestTokenSequence(instanceID) - logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID"). - OnError(err). - Errorf("could not get current sequence for token check") - token, viewErr := repo.View.TokenByIDs(tokenID, userID, instanceID) if viewErr != nil && !caos_errs.IsNotFound(viewErr) { return nil, viewErr } if caos_errs.IsNotFound(viewErr) { + sequence, err := repo.View.GetLatestTokenSequence(ctx, instanceID) + logging.WithFields("instanceID", instanceID, "userID", userID, "tokenID", tokenID). + OnError(err). + Errorf("could not get current sequence for token check") + token = new(model.TokenView) token.ID = tokenID token.UserID = userID diff --git a/internal/authz/repository/eventsourcing/eventstore/user_membership.go b/internal/authz/repository/eventsourcing/eventstore/user_membership.go index b7e95b603a..ae9cea03de 100644 --- a/internal/authz/repository/eventsourcing/eventstore/user_membership.go +++ b/internal/authz/repository/eventsourcing/eventstore/user_membership.go @@ -12,17 +12,17 @@ type UserMembershipRepo struct { Queries *query.Queries } -func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context) (_ []*authz.Membership, err error) { +func (repo *UserMembershipRepo) SearchMyMemberships(ctx context.Context, orgID string) (_ []*authz.Membership, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() - memberships, err := repo.searchUserMemberships(ctx) + memberships, err := repo.searchUserMemberships(ctx, orgID) if err != nil { return nil, err } return userMembershipsToMemberships(memberships), nil } -func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ []*query.Membership, err error) { +func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context, orgID string) (_ []*query.Membership, err error) { ctx, span := tracing.NewSpan(ctx) defer func() { span.EndWithError(err) }() ctxData := authz.GetCtxData(ctx) @@ -30,11 +30,11 @@ func (repo *UserMembershipRepo) searchUserMemberships(ctx context.Context) (_ [] if err != nil { return nil, err } - orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(ctxData.OrgID, authz.GetInstance(ctx).InstanceID()) + orgIDsQuery, err := query.NewMembershipResourceOwnersSearchQuery(orgID, authz.GetInstance(ctx).InstanceID()) if err != nil { return nil, err } - grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(ctxData.OrgID) + grantedIDQuery, err := query.NewMembershipGrantedOrgIDSearchQuery(orgID) if err != nil { return nil, err } diff --git a/internal/authz/repository/eventsourcing/repository.go b/internal/authz/repository/eventsourcing/repository.go index c3f212d96c..2df593f11d 100644 --- a/internal/authz/repository/eventsourcing/repository.go +++ b/internal/authz/repository/eventsourcing/repository.go @@ -18,8 +18,8 @@ type EsRepository struct { eventstore.TokenVerifierRepo } -func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure bool) (repository.Repository, error) { - es, err := v1.Start(dbClient) +func Start(queries *query.Queries, dbClient *database.DB, keyEncryptionAlgorithm crypto.EncryptionAlgorithm, externalSecure, allowOrderByCreationDate bool) (repository.Repository, error) { + es, err := v1.Start(dbClient, allowOrderByCreationDate) if err != nil { return nil, err } diff --git a/internal/authz/repository/eventsourcing/view/sequence.go b/internal/authz/repository/eventsourcing/view/sequence.go index 6810e420d6..18b577c0cd 100644 --- a/internal/authz/repository/eventsourcing/view/sequence.go +++ b/internal/authz/repository/eventsourcing/view/sequence.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" "github.com/zitadel/zitadel/internal/view/repository" ) @@ -13,6 +15,6 @@ func (v *View) saveCurrentSequence(viewName string, event *models.Event) error { return repository.SaveCurrentSequence(v.Db, sequencesTable, viewName, event.InstanceID, event.Sequence, event.CreationDate) } -func (v *View) latestSequence(viewName, instanceID string) (*repository.CurrentSequence, error) { - return repository.LatestSequence(v.Db, sequencesTable, viewName, instanceID) +func (v *View) latestSequence(ctx context.Context, viewName, instanceID string) (*repository.CurrentSequence, error) { + return repository.LatestSequence(v.Db, v.TimeTravel(ctx, sequencesTable), viewName, instanceID) } diff --git a/internal/authz/repository/eventsourcing/view/token.go b/internal/authz/repository/eventsourcing/view/token.go index 486d72008d..cfd0c8b9cf 100644 --- a/internal/authz/repository/eventsourcing/view/token.go +++ b/internal/authz/repository/eventsourcing/view/token.go @@ -1,6 +1,8 @@ package view import ( + "context" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore/v1/models" usr_view "github.com/zitadel/zitadel/internal/user/repository/view" @@ -40,8 +42,8 @@ func (v *View) DeleteSessionTokens(agentID, userID, instanceID string, event *mo return v.ProcessedTokenSequence(event) } -func (v *View) GetLatestTokenSequence(instanceID string) (*repository.CurrentSequence, error) { - return v.latestSequence(tokenTable, instanceID) +func (v *View) GetLatestTokenSequence(ctx context.Context, instanceID string) (*repository.CurrentSequence, error) { + return v.latestSequence(ctx, tokenTable, instanceID) } func (v *View) ProcessedTokenSequence(event *models.Event) error { diff --git a/internal/authz/repository/eventsourcing/view/view.go b/internal/authz/repository/eventsourcing/view/view.go index a55dec7699..c3ee5b79ac 100644 --- a/internal/authz/repository/eventsourcing/view/view.go +++ b/internal/authz/repository/eventsourcing/view/view.go @@ -1,17 +1,21 @@ package view import ( + "context" + + "github.com/jinzhu/gorm" + + "github.com/zitadel/zitadel/internal/api/call" "github.com/zitadel/zitadel/internal/database" "github.com/zitadel/zitadel/internal/id" "github.com/zitadel/zitadel/internal/query" - - "github.com/jinzhu/gorm" ) type View struct { Db *gorm.DB Query *query.Queries idGenerator id.Generator + client *database.DB } func StartView(sqlClient *database.DB, idGenerator id.Generator, queries *query.Queries) (*View, error) { @@ -23,9 +27,14 @@ func StartView(sqlClient *database.DB, idGenerator id.Generator, queries *query. Db: gorm, idGenerator: idGenerator, Query: queries, + client: sqlClient, }, nil } func (v *View) Health() (err error) { return v.Db.DB().Ping() } + +func (v *View) TimeTravel(ctx context.Context, tableName string) string { + return tableName + v.client.Timetravel(call.Took(ctx)) +} diff --git a/internal/authz/repository/user_membership.go b/internal/authz/repository/user_membership.go index 105a51ba5e..a1b1fc78be 100644 --- a/internal/authz/repository/user_membership.go +++ b/internal/authz/repository/user_membership.go @@ -7,5 +7,5 @@ import ( ) type UserMembershipRepository interface { - SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) + SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error) } diff --git a/internal/command/command.go b/internal/command/command.go index 485bf563cf..82f2a4c455 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -29,6 +29,9 @@ import ( type Commands struct { httpClient *http.Client + checkPermission permissionCheck + newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, codeAlg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) + eventstore *eventstore.Eventstore static static.Storage idGenerator id.Generator @@ -59,7 +62,8 @@ type Commands struct { certificateLifetime time.Duration } -func StartCommands(es *eventstore.Eventstore, +func StartCommands( + es *eventstore.Eventstore, defaults sd.SystemDefaults, zitadelRoles []authz.RoleMapping, staticStore static.Storage, @@ -76,6 +80,7 @@ func StartCommands(es *eventstore.Eventstore, oidcEncryption, samlEncryption crypto.EncryptionAlgorithm, httpClient *http.Client, + membershipsResolver authz.MembershipsResolver, ) (repo *Commands, err error) { if externalDomain == "" { return nil, errors.ThrowInvalidArgument(nil, "COMMAND-Df21s", "no external domain specified") @@ -102,6 +107,10 @@ func StartCommands(es *eventstore.Eventstore, certificateAlgorithm: samlEncryption, webauthnConfig: webAuthN, httpClient: httpClient, + checkPermission: func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) { + return authz.CheckPermission(ctx, membershipsResolver, zitadelRoles, permission, orgID, resourceID, allowSelf) + }, + newEmailCode: newEmailCode, } instance_repo.RegisterEventMappers(repo.eventstore) diff --git a/internal/command/crypto.go b/internal/command/crypto.go index 2313e86b62..404b2221f9 100644 --- a/internal/command/crypto.go +++ b/internal/command/crypto.go @@ -10,24 +10,33 @@ import ( "github.com/zitadel/zitadel/internal/errors" ) -func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, expiry time.Duration, err error) { +type CryptoCodeWithExpiry struct { + Crypted *crypto.CryptoValue + Plain string + Expiry time.Duration +} + +func newCryptoCodeWithExpiry(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (*CryptoCodeWithExpiry, error) { config, err := secretGeneratorConfig(ctx, filter, typ) if err != nil { - return nil, -1, err + return nil, err } + code := new(CryptoCodeWithExpiry) switch a := alg.(type) { case crypto.HashAlgorithm: - value, _, err = crypto.NewCode(crypto.NewHashGenerator(*config, a)) + code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewHashGenerator(*config, a)) case crypto.EncryptionAlgorithm: - value, _, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a)) + code.Crypted, code.Plain, err = crypto.NewCode(crypto.NewEncryptionGenerator(*config, a)) default: - return nil, -1, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal") + return nil, errors.ThrowInternal(nil, "COMMA-RreV6", "Errors.Internal") } if err != nil { - return nil, -1, err + return nil, err } - return value, config.Expiry, nil + + code.Expiry = config.Expiry + return code, nil } func newCryptoCodeWithPlain(ctx context.Context, filter preparation.FilterToQueryReducer, typ domain.SecretGeneratorType, alg crypto.Crypto) (value *crypto.CryptoValue, plain string, err error) { diff --git a/internal/command/device_auth.go b/internal/command/device_auth.go new file mode 100644 index 0000000000..6c3e1a3cfa --- /dev/null +++ b/internal/command/device_auth.go @@ -0,0 +1,113 @@ +package command + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/deviceauth" +) + +func (c *Commands) AddDeviceAuth(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (string, *domain.ObjectDetails, error) { + aggrID, err := c.idGenerator.Next() + if err != nil { + return "", nil, err + } + + aggr := deviceauth.NewAggregate(aggrID, authz.GetInstance(ctx).InstanceID()) + model := NewDeviceAuthWriteModel(aggrID, aggr.ResourceOwner) + + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewAddedEvent( + ctx, + aggr, + clientID, + deviceCode, + userCode, + expires, + scopes, + )) + if err != nil { + return "", nil, err + } + err = AppendAndReduce(model, pushedEvents...) + if err != nil { + return "", nil, err + } + + return model.AggregateID, writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) ApproveDeviceAuth(ctx context.Context, id, subject string) (*domain.ObjectDetails, error) { + model, err := c.getDeviceAuthWriteModelByID(ctx, id) + if err != nil { + return nil, err + } + if !model.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound") + } + aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID) + + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewApprovedEvent(ctx, aggr, subject)) + if err != nil { + return nil, err + } + err = AppendAndReduce(model, pushedEvents...) + if err != nil { + return nil, err + } + + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) CancelDeviceAuth(ctx context.Context, id string, reason domain.DeviceAuthCanceled) (*domain.ObjectDetails, error) { + model, err := c.getDeviceAuthWriteModelByID(ctx, id) + if err != nil { + return nil, err + } + if !model.State.Exists() { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound") + } + aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID) + + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewCanceledEvent(ctx, aggr, reason)) + if err != nil { + return nil, err + } + err = AppendAndReduce(model, pushedEvents...) + if err != nil { + return nil, err + } + + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) RemoveDeviceAuth(ctx context.Context, id string) (*domain.ObjectDetails, error) { + model, err := c.getDeviceAuthWriteModelByID(ctx, id) + if err != nil { + return nil, err + } + aggr := deviceauth.NewAggregate(model.AggregateID, model.InstanceID) + + pushedEvents, err := c.eventstore.Push(ctx, deviceauth.NewRemovedEvent(ctx, aggr, model.ClientID, model.DeviceCode, model.UserCode)) + if err != nil { + return nil, err + } + err = AppendAndReduce(model, pushedEvents...) + if err != nil { + return nil, err + } + + return writeModelToObjectDetails(&model.WriteModel), nil +} + +func (c *Commands) getDeviceAuthWriteModelByID(ctx context.Context, id string) (*DeviceAuthWriteModel, error) { + model := &DeviceAuthWriteModel{WriteModel: eventstore.WriteModel{AggregateID: id}} + err := c.eventstore.FilterToQueryReducer(ctx, model) + if err != nil { + return nil, err + } + return model, nil +} diff --git a/internal/command/device_auth_model.go b/internal/command/device_auth_model.go new file mode 100644 index 0000000000..2ea52a39ab --- /dev/null +++ b/internal/command/device_auth_model.go @@ -0,0 +1,61 @@ +package command + +import ( + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/deviceauth" +) + +type DeviceAuthWriteModel struct { + eventstore.WriteModel + + ClientID string + DeviceCode string + UserCode string + Expires time.Time + Scopes []string + Subject string + State domain.DeviceAuthState +} + +func NewDeviceAuthWriteModel(aggrID, resourceOwner string) *DeviceAuthWriteModel { + return &DeviceAuthWriteModel{ + WriteModel: eventstore.WriteModel{ + AggregateID: aggrID, + ResourceOwner: resourceOwner, + }, + } +} + +func (m *DeviceAuthWriteModel) Reduce() error { + for _, event := range m.Events { + switch e := event.(type) { + case *deviceauth.AddedEvent: + m.ClientID = e.ClientID + m.DeviceCode = e.DeviceCode + m.UserCode = e.UserCode + m.Expires = e.Expires + m.Scopes = e.Scopes + m.State = e.State + case *deviceauth.ApprovedEvent: + m.Subject = e.Subject + m.State = domain.DeviceAuthStateApproved + case *deviceauth.CanceledEvent: + m.State = e.Reason.State() + case *deviceauth.RemovedEvent: + m.State = domain.DeviceAuthStateRemoved + } + } + + return m.WriteModel.Reduce() +} + +func (m *DeviceAuthWriteModel) Query() *eventstore.SearchQueryBuilder { + return eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + AddQuery(). + AggregateTypes(deviceauth.AggregateType). + AggregateIDs(m.AggregateID). + Builder() +} diff --git a/internal/command/device_auth_test.go b/internal/command/device_auth_test.go new file mode 100644 index 0000000000..d0d3dd8281 --- /dev/null +++ b/internal/command/device_auth_test.go @@ -0,0 +1,481 @@ +package command + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/id" + id_mock "github.com/zitadel/zitadel/internal/id/mock" + "github.com/zitadel/zitadel/internal/repository/deviceauth" +) + +func TestCommands_AddDeviceAuth(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instance1") + idErr := errors.New("idErr") + pushErr := errors.New("pushErr") + now := time.Now() + + unique := deviceauth.NewAddUniqueConstraints("client_id", "123", "456") + require.Len(t, unique, 2) + + type fields struct { + eventstore *eventstore.Eventstore + idGenerator id.Generator + } + type args struct { + ctx context.Context + clientID string + deviceCode string + userCode string + expires time.Time + scopes []string + } + tests := []struct { + name string + fields fields + args args + wantID string + wantDetails *domain.ObjectDetails + wantErr error + }{ + { + name: "idGenerator error", + fields: fields{ + eventstore: eventstoreExpect(t), + idGenerator: func() id.Generator { + m := id_mock.NewMockGenerator(gomock.NewController(t)) + m.EXPECT().Next().Return("", idErr) + return m + }(), + }, + args: args{ + ctx: ctx, + clientID: "client_id", + deviceCode: "123", + userCode: "456", + expires: now, + scopes: []string{"a", "b", "c"}, + }, + wantErr: idErr, + }, + { + name: "success", + fields: fields{ + eventstore: eventstoreExpect(t, expectPush( + []*repository.Event{ + eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + )), + }, + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]), + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]), + )), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + clientID: "client_id", + deviceCode: "123", + userCode: "456", + expires: now, + scopes: []string{"a", "b", "c"}, + }, + wantID: "1999", + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "push error", + fields: fields{ + eventstore: eventstoreExpect(t, expectPushFailed(pushErr, + []*repository.Event{ + eventFromEventPusherWithInstanceID("instance1", deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + )), + }, + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]), + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]), + )), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "1999"), + }, + args: args{ + ctx: authz.WithInstanceID(context.Background(), "instance1"), + clientID: "client_id", + deviceCode: "123", + userCode: "456", + expires: now, + scopes: []string{"a", "b", "c"}, + }, + wantErr: pushErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + idGenerator: tt.fields.idGenerator, + } + gotID, gotDetails, err := c.AddDeviceAuth(tt.args.ctx, tt.args.clientID, tt.args.deviceCode, tt.args.userCode, tt.args.expires, tt.args.scopes) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, gotID, tt.wantID) + assert.Equal(t, gotDetails, tt.wantDetails) + }) + } +} + +func TestCommands_ApproveDeviceAuth(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instance1") + now := time.Now() + pushErr := errors.New("pushErr") + + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + id string + subject string + } + tests := []struct { + name string + fields fields + args args + wantDetails *domain.ObjectDetails + wantErr error + }{ + { + name: "not found error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithInstanceID("instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + ), + eventFromEventPusherWithInstanceID("instance1", + deviceauth.NewRemovedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", + ), + ), + ), + ), + }, + args: args{ctx, "1999", "subj"}, + wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-Hief9", "Errors.DeviceAuth.NotFound"), + }, + { + name: "push error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPushFailed(pushErr, + []*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), "subj", + ), + )}, + ), + ), + }, + args: args{ctx, "1999", "subj"}, + wantErr: pushErr, + }, + { + name: "success", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPush([]*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewApprovedEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), "subj", + ), + )}), + ), + }, + args: args{ctx, "1999", "subj"}, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + gotDetails, err := c.ApproveDeviceAuth(tt.args.ctx, tt.args.id, tt.args.subject) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, gotDetails, tt.wantDetails) + }) + } +} + +func TestCommands_CancelDeviceAuth(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instance1") + now := time.Now() + pushErr := errors.New("pushErr") + + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + id string + reason domain.DeviceAuthCanceled + } + tests := []struct { + name string + fields fields + args args + wantDetails *domain.ObjectDetails + wantErr error + }{ + { + name: "not found error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter( + eventFromEventPusherWithInstanceID("instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + ), + eventFromEventPusherWithInstanceID("instance1", + deviceauth.NewRemovedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", + ), + ), + ), + ), + }, + args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-gee5A", "Errors.DeviceAuth.NotFound"), + }, + { + name: "push error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPushFailed(pushErr, + []*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewCanceledEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), + domain.DeviceAuthCanceledDenied, + ), + )}, + ), + ), + }, + args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + wantErr: pushErr, + }, + { + name: "success/denied", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPush([]*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewCanceledEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), + domain.DeviceAuthCanceledDenied, + ), + )}), + ), + }, + args: args{ctx, "1999", domain.DeviceAuthCanceledDenied}, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + { + name: "success/expired", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPush([]*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewCanceledEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), + domain.DeviceAuthCanceledExpired, + ), + )}), + ), + }, + args: args{ctx, "1999", domain.DeviceAuthCanceledExpired}, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + gotDetails, err := c.CancelDeviceAuth(tt.args.ctx, tt.args.id, tt.args.reason) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, gotDetails, tt.wantDetails) + }) + } +} + +func TestCommands_RemoveDeviceAuth(t *testing.T) { + ctx := authz.WithInstanceID(context.Background(), "instance1") + now := time.Now() + pushErr := errors.New("pushErr") + + unique := deviceauth.NewRemoveUniqueConstraints("client_id", "123", "456") + require.Len(t, unique, 2) + + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + ctx context.Context + id string + } + tests := []struct { + name string + fields fields + args args + wantDetails *domain.ObjectDetails + wantErr error + }{ + { + name: "push error", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPushFailed(pushErr, + []*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewRemovedEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", + ), + )}, + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]), + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]), + ), + ), + }, + args: args{ctx, "1999"}, + wantErr: pushErr, + }, + { + name: "success", + fields: fields{ + eventstore: eventstoreExpect(t, + expectFilter(eventFromEventPusherWithInstanceID( + "instance1", + deviceauth.NewAddedEvent( + ctx, + deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", now, + []string{"a", "b", "c"}, + ), + )), + expectPush( + []*repository.Event{eventFromEventPusherWithInstanceID( + "instance1", deviceauth.NewRemovedEvent( + ctx, deviceauth.NewAggregate("1999", "instance1"), + "client_id", "123", "456", + ), + )}, + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[0]), + uniqueConstraintsFromEventConstraintWithInstanceID("instance1", unique[1]), + ), + ), + }, + args: args{ctx, "1999"}, + wantDetails: &domain.ObjectDetails{ + ResourceOwner: "instance1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + gotDetails, err := c.RemoveDeviceAuth(tt.args.ctx, tt.args.id) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, gotDetails, tt.wantDetails) + }) + } +} diff --git a/internal/command/email.go b/internal/command/email.go index 4e2c3d973b..0bfbcd6af6 100644 --- a/internal/command/email.go +++ b/internal/command/email.go @@ -2,7 +2,6 @@ package command import ( "context" - "time" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" @@ -12,12 +11,18 @@ import ( type Email struct { Address domain.EmailAddress Verified bool + + // ReturnCode is used if the Verified field is false + ReturnCode bool + + // URLTemplate can be used to specify a custom link to be sent in the mail verification + URLTemplate string } func (e *Email) Validate() error { return e.Address.Validate() } -func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { +func newEmailCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) { return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyEmailCode, alg) } diff --git a/internal/command/instance.go b/internal/command/instance.go index 436b61b2bf..415fae3a2a 100644 --- a/internal/command/instance.go +++ b/internal/command/instance.go @@ -333,8 +333,9 @@ func (c *Commands) SetUpInstance(ctx context.Context, setup *InstanceSetup) (str validations = append(validations, prepareAddUserMachineKey(machineKey, c.machineKeySize)) } } else if setup.Org.Human != nil { + setup.Org.Human.ID = userID validations = append(validations, - AddHumanCommand(userAgg, setup.Org.Human, c.userPasswordAlg, c.userEncryption), + c.AddHumanCommand(setup.Org.Human, orgID, c.userPasswordAlg, c.userEncryption, true), ) } diff --git a/internal/command/main_test.go b/internal/command/main_test.go index 237f98488a..2f516133f4 100644 --- a/internal/command/main_test.go +++ b/internal/command/main_test.go @@ -10,6 +10,7 @@ import ( "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/eventstore/repository/mock" @@ -248,3 +249,15 @@ func (m *mockInstance) RequestedHost() string { func (m *mockInstance) SecurityPolicyAllowedOrigins() []string { return nil } + +func newMockPermissionCheckAllowed() permissionCheck { + return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) { + return nil + } +} + +func newMockPermissionCheckNotAllowed() permissionCheck { + return func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) { + return errors.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied") + } +} diff --git a/internal/command/org.go b/internal/command/org.go index 04f38877d4..c9fd3dbd4a 100644 --- a/internal/command/org.go +++ b/internal/command/org.go @@ -11,7 +11,7 @@ import ( "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/org" "github.com/zitadel/zitadel/internal/repository/project" - user_repo "github.com/zitadel/zitadel/internal/repository/user" + "github.com/zitadel/zitadel/internal/repository/user" ) type OrgSetup struct { @@ -22,22 +22,13 @@ type OrgSetup struct { Roles []string } -func (c *Commands) SetUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, *domain.ObjectDetails, error) { - existingOrg, err := c.getOrgWriteModelByID(ctx, orgID) +func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID string, userIDs ...string) (userID string, token string, machineKey *MachineKey, details *domain.ObjectDetails, err error) { + userID, err = c.idGenerator.Next() if err != nil { - return "", nil, err + return "", "", nil, nil, err } - if existingOrg != nil { - return "", nil, errors.ThrowPreconditionFailed(nil, "COMMAND-poaj2", "Errors.Org.AlreadyExisting") - } - - userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) - return userID, details, err -} - -func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, userID string, userIDs ...string) (string, string, *MachineKey, *domain.ObjectDetails, error) { orgAgg := org.NewAggregate(orgID) - userAgg := user_repo.NewAggregate(userID, orgID) + userAgg := user.NewAggregate(userID, orgID) roles := []string{domain.RoleOrgOwner} if len(o.Roles) > 0 { @@ -49,9 +40,9 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user } var pat *PersonalAccessToken - var machineKey *MachineKey if o.Human != nil { - validations = append(validations, AddHumanCommand(userAgg, o.Human, c.userPasswordAlg, c.userEncryption)) + o.Human.ID = userID + validations = append(validations, c.AddHumanCommand(o.Human, orgID, c.userPasswordAlg, c.userEncryption, true)) } else if o.Machine != nil { validations = append(validations, AddMachineCommand(userAgg, o.Machine.Machine)) if o.Machine.Pat != nil { @@ -89,7 +80,6 @@ func (c *Commands) setUpOrgWithIDs(ctx context.Context, o *OrgSetup, orgID, user return "", "", nil, nil, err } - var token string if pat != nil { token = pat.Token } @@ -107,12 +97,7 @@ func (c *Commands) SetUpOrg(ctx context.Context, o *OrgSetup, userIDs ...string) return "", nil, err } - userID, err := c.idGenerator.Next() - if err != nil { - return "", nil, err - } - - userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userID, userIDs...) + userID, _, _, details, err := c.setUpOrgWithIDs(ctx, o, orgID, userIDs...) return userID, details, err } @@ -365,9 +350,9 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce ResourceOwner(orgID). OrderAsc(). AddQuery(). - AggregateTypes(user_repo.AggregateType). + AggregateTypes(user.AggregateType). EventTypes( - user_repo.UserIDPLinkAddedType, user_repo.UserIDPLinkRemovedType, user_repo.UserIDPLinkCascadeRemovedType, + user.UserIDPLinkAddedType, user.UserIDPLinkRemovedType, user.UserIDPLinkCascadeRemovedType, ).Builder()) if err != nil { return nil, err @@ -375,13 +360,13 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce links := make([]*domain.UserIDPLink, 0) for _, event := range events { switch eventTyped := event.(type) { - case *user_repo.UserIDPLinkAddedEvent: + case *user.UserIDPLinkAddedEvent: links = append(links, &domain.UserIDPLink{ IDPConfigID: eventTyped.IDPConfigID, ExternalUserID: eventTyped.ExternalUserID, DisplayName: eventTyped.DisplayName, }) - case *user_repo.UserIDPLinkRemovedEvent: + case *user.UserIDPLinkRemovedEvent: for i := range links { if links[i].ExternalUserID == eventTyped.ExternalUserID && links[i].IDPConfigID == eventTyped.IDPConfigID { @@ -392,7 +377,7 @@ func OrgUserIDPLinks(ctx context.Context, filter preparation.FilterToQueryReduce } } - case *user_repo.UserIDPLinkCascadeRemovedEvent: + case *user.UserIDPLinkCascadeRemovedEvent: for i := range links { if links[i].ExternalUserID == eventTyped.ExternalUserID && links[i].IDPConfigID == eventTyped.IDPConfigID { @@ -495,14 +480,14 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI ResourceOwner(orgID). OrderAsc(). AddQuery(). - AggregateTypes(user_repo.AggregateType). + AggregateTypes(user.AggregateType). EventTypes( - user_repo.HumanAddedType, - user_repo.MachineAddedEventType, - user_repo.HumanRegisteredType, - user_repo.UserDomainClaimedType, - user_repo.UserUserNameChangedType, - user_repo.UserRemovedType, + user.HumanAddedType, + user.MachineAddedEventType, + user.HumanRegisteredType, + user.UserDomainClaimedType, + user.UserUserNameChangedType, + user.UserRemovedType, ).Builder()) if err != nil { return nil, err @@ -511,25 +496,25 @@ func OrgUsers(ctx context.Context, filter preparation.FilterToQueryReducer, orgI users := make([]userIDName, 0) for _, event := range events { switch eventTyped := event.(type) { - case *user_repo.HumanAddedEvent: + case *user.HumanAddedEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) - case *user_repo.MachineAddedEvent: + case *user.MachineAddedEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) - case *user_repo.HumanRegisteredEvent: + case *user.HumanRegisteredEvent: users = append(users, userIDName{eventTyped.UserName, eventTyped.Aggregate().ID}) - case *user_repo.DomainClaimedEvent: + case *user.DomainClaimedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i].name = eventTyped.UserName } } - case *user_repo.UsernameChangedEvent: + case *user.UsernameChangedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i].name = eventTyped.UserName } } - case *user_repo.UserRemovedEvent: + case *user.UserRemovedEvent: for i := range users { if users[i].id == eventTyped.Aggregate().ID { users[i] = users[len(users)-1] diff --git a/internal/command/permission.go b/internal/command/permission.go new file mode 100644 index 0000000000..138df0a44a --- /dev/null +++ b/internal/command/permission.go @@ -0,0 +1,11 @@ +package command + +import ( + "context" +) + +type permissionCheck func(ctx context.Context, permission, orgID, resourceID string, allowSelf bool) (err error) + +const ( + permissionUserWrite = "user.write" +) diff --git a/internal/command/phone.go b/internal/command/phone.go index 109a60975e..30cabb6fcb 100644 --- a/internal/command/phone.go +++ b/internal/command/phone.go @@ -2,7 +2,6 @@ package command import ( "context" - "time" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" @@ -14,6 +13,6 @@ type Phone struct { Verified bool } -func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { +func newPhoneCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) { return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeVerifyPhoneCode, alg) } diff --git a/internal/command/user.go b/internal/command/user.go index 96ca0d1e68..34ba7c521a 100644 --- a/internal/command/user.go +++ b/internal/command/user.go @@ -439,7 +439,7 @@ func ExistsUser(ctx context.Context, filter preparation.FilterToQueryReducer, id return exists, nil } -func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (value *crypto.CryptoValue, expiry time.Duration, err error) { +func newUserInitCode(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) { return newCryptoCodeWithExpiry(ctx, filter, domain.SecretGeneratorTypeInitCode, alg) } diff --git a/internal/command/user_human.go b/internal/command/user_human.go index 3d55ccf53e..0bb075a345 100644 --- a/internal/command/user_human.go +++ b/internal/command/user_human.go @@ -27,6 +27,8 @@ func (c *Commands) getHuman(ctx context.Context, userID, resourceowner string) ( } type AddHuman struct { + // ID is optional, if empty it will be generated + ID string // Username is required Username string // FirstName is required @@ -43,63 +45,98 @@ type AddHuman struct { PreferredLanguage language.Tag // Gender is required Gender domain.Gender - //Phone represents an international phone number + // Phone represents an international phone number Phone Phone - //Password is optional + // Password is optional Password string - //BcryptedPassword is optional + // BcryptedPassword is optional BcryptedPassword string - //PasswordChangeRequired is used if the `Password`-field is set + // PasswordChangeRequired is used if the `Password`-field is set PasswordChangeRequired bool Passwordless bool ExternalIDP bool Register bool + Metadata []*AddMetadataEntry + + // Details are set after a successful execution of the command + Details *domain.ObjectDetails + + // EmailCode is set by the command + EmailCode *string } -func (c *Commands) AddHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) { - existingHuman, err := c.getHumanWriteModelByID(ctx, userID, resourceOwner) - if err != nil { - return nil, err +func (h *AddHuman) Validate() (err error) { + if err := h.Email.Validate(); err != nil { + return err } - if isUserStateExists(existingHuman.UserState) { - return nil, errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting") + if h.Username = strings.TrimSpace(h.Username); h.Username == "" { + return errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") } - return c.addHumanWithID(ctx, resourceOwner, userID, human) + if h.FirstName = strings.TrimSpace(h.FirstName); h.FirstName == "" { + return errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty") + } + if h.LastName = strings.TrimSpace(h.LastName); h.LastName == "" { + return errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty") + } + h.ensureDisplayName() + + if h.Phone.Number != "" { + if h.Phone.Number, err = h.Phone.Number.Normalize(); err != nil { + return err + } + } + + for _, metadataEntry := range h.Metadata { + if err := metadataEntry.Valid(); err != nil { + return err + } + } + return nil } -func (c *Commands) addHumanWithID(ctx context.Context, resourceOwner string, userID string, human *AddHuman) (*domain.HumanDetails, error) { - agg := user.NewAggregate(userID, resourceOwner) - cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, AddHumanCommand(agg, human, c.userPasswordAlg, c.userEncryption)) +type AddMetadataEntry struct { + Key string + Value []byte +} + +func (m *AddMetadataEntry) Valid() error { + if m.Key = strings.TrimSpace(m.Key); m.Key == "" { + return errors.ThrowInvalidArgument(nil, "USER-Drght", "Errors.User.Metadata.KeyEmpty") + } + if len(m.Value) == 0 { + return errors.ThrowInvalidArgument(nil, "USER-Dbgth", "Errors.User.Metadata.ValueEmpty") + } + return nil +} + +func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman, allowInitMail bool) (err error) { + if resourceOwner == "" { + return errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal") + } + cmds, err := preparation.PrepareCommands(ctx, c.eventstore.Filter, + c.AddHumanCommand( + human, + resourceOwner, + c.userPasswordAlg, + c.userEncryption, + allowInitMail, + )) if err != nil { - return nil, err + return err } events, err := c.eventstore.Push(ctx, cmds...) if err != nil { - return nil, err + return err + } + human.Details = &domain.ObjectDetails{ + Sequence: events[len(events)-1].Sequence(), + EventDate: events[len(events)-1].CreationDate(), + ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, } - return &domain.HumanDetails{ - ID: userID, - ObjectDetails: domain.ObjectDetails{ - Sequence: events[len(events)-1].Sequence(), - EventDate: events[len(events)-1].CreationDate(), - ResourceOwner: events[len(events)-1].Aggregate().ResourceOwner, - }, - }, nil -} - -func (c *Commands) AddHuman(ctx context.Context, resourceOwner string, human *AddHuman) (*domain.HumanDetails, error) { - if resourceOwner == "" { - return nil, errors.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal") - } - userID, err := c.idGenerator.Next() - if err != nil { - return nil, err - } - - return c.addHumanWithID(ctx, resourceOwner, userID, human) + return nil } type humanCreationCommand interface { @@ -108,30 +145,18 @@ type humanCreationCommand interface { AddPasswordData(secret *crypto.CryptoValue, changeRequired bool) } -func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm) preparation.Validation { +func (c *Commands) AddHumanCommand(human *AddHuman, orgID string, passwordAlg crypto.HashAlgorithm, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) preparation.Validation { return func() (_ preparation.CreateCommands, err error) { - if err := human.Email.Validate(); err != nil { + if err := human.Validate(); err != nil { return nil, err } - if human.Username = strings.TrimSpace(human.Username); human.Username == "" { - return nil, errors.ThrowInvalidArgument(nil, "V2-zzad3", "Errors.Invalid.Argument") - } - - if human.FirstName = strings.TrimSpace(human.FirstName); human.FirstName == "" { - return nil, errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty") - } - if human.LastName = strings.TrimSpace(human.LastName); human.LastName == "" { - return nil, errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty") - } - human.ensureDisplayName() - - if human.Phone.Number != "" { - if human.Phone.Number, err = human.Phone.Number.Normalize(); err != nil { - return nil, err - } - } return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + if err := c.addHumanCommandCheckID(ctx, filter, human, orgID); err != nil { + return nil, err + } + a := user.NewAggregate(human.ID, orgID) + domainPolicy, err := domainPolicyWriteModel(ctx, filter, a.ResourceOwner) if err != nil { return nil, err @@ -176,55 +201,30 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash createCmd.AddPhoneData(human.Phone.Number) } - if human.Password != "" { - if err = humanValidatePassword(ctx, filter, human.Password); err != nil { - return nil, err - } - - secret, err := crypto.Hash([]byte(human.Password), passwordAlg) - if err != nil { - return nil, err - } - createCmd.AddPasswordData(secret, human.PasswordChangeRequired) - } - - if human.BcryptedPassword != "" { - createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired) + if err := addHumanCommandPassword(ctx, filter, createCmd, human, passwordAlg); err != nil { + return nil, err } cmds := make([]eventstore.Command, 0, 3) cmds = append(cmds, createCmd) - if human.Email.Verified { - cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate)) - } - //add init code if - // email not verified or - // user not registered and password set - if human.shouldAddInitCode() { - value, expiry, err := newUserInitCode(ctx, filter, codeAlg) - if err != nil { - return nil, err - } - cmds = append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) - } else { - if !human.Email.Verified { - value, expiry, err := newEmailCode(ctx, filter, codeAlg) - if err != nil { - return nil, err - } - cmds = append(cmds, user.NewHumanEmailCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) - } + cmds, err = c.addHumanCommandEmail(ctx, filter, cmds, a, human, codeAlg, allowInitMail) + if err != nil { + return nil, err } - if human.Phone.Verified { - cmds = append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)) - } else if human.Phone.Number != "" { - value, expiry, err := newPhoneCode(ctx, filter, codeAlg) - if err != nil { - return nil, err - } - cmds = append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, value, expiry)) + cmds, err = c.addHumanCommandPhone(ctx, filter, cmds, a, human, codeAlg) + if err != nil { + return nil, err + } + + for _, metadataEntry := range human.Metadata { + cmds = append(cmds, user.NewMetadataSetEvent( + ctx, + &a.Aggregate, + metadataEntry.Key, + metadataEntry.Value, + )) } return cmds, nil @@ -232,6 +232,85 @@ func AddHumanCommand(a *user.Aggregate, human *AddHuman, passwordAlg crypto.Hash } } +func (c *Commands) addHumanCommandEmail(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm, allowInitMail bool) ([]eventstore.Command, error) { + if human.Email.Verified { + cmds = append(cmds, user.NewHumanEmailVerifiedEvent(ctx, &a.Aggregate)) + } + + // if allowInitMail, used for v1 api (system, admin, mgmt, auth): + // add init code if + // email not verified or + // user not registered and password set + if allowInitMail && human.shouldAddInitCode() { + initCode, err := newUserInitCode(ctx, filter, codeAlg) + if err != nil { + return nil, err + } + return append(cmds, user.NewHumanInitialCodeAddedEvent(ctx, &a.Aggregate, initCode.Crypted, initCode.Expiry)), nil + } + if !human.Email.Verified { + emailCode, err := c.newEmailCode(ctx, filter, codeAlg) + if err != nil { + return nil, err + } + if human.Email.ReturnCode { + human.EmailCode = &emailCode.Plain + } + return append(cmds, user.NewHumanEmailCodeAddedEventV2(ctx, &a.Aggregate, emailCode.Crypted, emailCode.Expiry, human.Email.URLTemplate, human.Email.ReturnCode)), nil + } + return cmds, nil +} +func (c *Commands) addHumanCommandPhone(ctx context.Context, filter preparation.FilterToQueryReducer, cmds []eventstore.Command, a *user.Aggregate, human *AddHuman, codeAlg crypto.EncryptionAlgorithm) ([]eventstore.Command, error) { + if human.Phone.Number == "" { + return cmds, nil + } + if human.Phone.Verified { + return append(cmds, user.NewHumanPhoneVerifiedEvent(ctx, &a.Aggregate)), nil + } + phoneCode, err := newPhoneCode(ctx, filter, codeAlg) + if err != nil { + return nil, err + } + return append(cmds, user.NewHumanPhoneCodeAddedEvent(ctx, &a.Aggregate, phoneCode.Crypted, phoneCode.Expiry)), nil +} + +func (c *Commands) addHumanCommandCheckID(ctx context.Context, filter preparation.FilterToQueryReducer, human *AddHuman, orgID string) (err error) { + if human.ID == "" { + human.ID, err = c.idGenerator.Next() + if err != nil { + return err + } + } + existingHuman, err := humanWriteModelByID(ctx, filter, human.ID, orgID) + if err != nil { + return err + } + if isUserStateExists(existingHuman.UserState) { + return errors.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting") + } + return nil +} + +func addHumanCommandPassword(ctx context.Context, filter preparation.FilterToQueryReducer, createCmd humanCreationCommand, human *AddHuman, passwordAlg crypto.HashAlgorithm) (err error) { + if human.Password != "" { + if err = humanValidatePassword(ctx, filter, human.Password); err != nil { + return err + } + + secret, err := crypto.Hash([]byte(human.Password), passwordAlg) + if err != nil { + return err + } + createCmd.AddPasswordData(secret, human.PasswordChangeRequired) + return nil + } + + if human.BcryptedPassword != "" { + createCmd.AddPasswordData(crypto.FillHash([]byte(human.BcryptedPassword), passwordAlg), human.PasswordChangeRequired) + } + return nil +} + func userValidateDomain(ctx context.Context, a *user.Aggregate, username string, mustBeDomain bool, filter preparation.FilterToQueryReducer) error { if mustBeDomain { return nil @@ -507,7 +586,7 @@ func (c *Commands) createHuman(ctx context.Context, orgID string, human *domain. if human.Email != nil && human.EmailAddress != "" && human.IsEmailVerified { events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) } else { - emailCode, err := domain.NewEmailCode(emailCodeGenerator) + emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { return nil, nil, err } @@ -651,3 +730,17 @@ func (c *Commands) getHumanWriteModelByID(ctx context.Context, userID, resourceo } return humanWriteModel, nil } + +func humanWriteModelByID(ctx context.Context, filter preparation.FilterToQueryReducer, userID, resourceowner string) (*HumanWriteModel, error) { + humanWriteModel := NewHumanWriteModel(userID, resourceowner) + events, err := filter(ctx, humanWriteModel.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return humanWriteModel, nil + } + humanWriteModel.AppendEvents(events...) + err = humanWriteModel.Reduce() + return humanWriteModel, err +} diff --git a/internal/command/user_human_email.go b/internal/command/user_human_email.go index 22fbf7b464..66a4982863 100644 --- a/internal/command/user_human_email.go +++ b/internal/command/user_human_email.go @@ -4,6 +4,7 @@ import ( "context" "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" @@ -41,7 +42,7 @@ func (c *Commands) ChangeHumanEmail(ctx context.Context, email *domain.Email, em if email.IsEmailVerified { events = append(events, user.NewHumanEmailVerifiedEvent(ctx, userAgg)) } else { - emailCode, err := domain.NewEmailCode(emailCodeGenerator) + emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { return nil, err } @@ -113,7 +114,7 @@ func (c *Commands) CreateHumanEmailVerificationCode(ctx context.Context, userID, return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-3M9ds", "Errors.User.Email.AlreadyVerified") } userAgg := UserAggregateFromWriteModel(&existingEmail.WriteModel) - emailCode, err := domain.NewEmailCode(emailCodeGenerator) + emailCode, _, err := domain.NewEmailCode(emailCodeGenerator) if err != nil { return nil, err } diff --git a/internal/command/user_human_email_model.go b/internal/command/user_human_email_model.go index f332477496..d05881a79a 100644 --- a/internal/command/user_human_email_model.go +++ b/internal/command/user_human_email_model.go @@ -4,10 +4,9 @@ import ( "context" "time" - "github.com/zitadel/zitadel/internal/eventstore" - "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/repository/user" ) diff --git a/internal/command/user_human_otp.go b/internal/command/user_human_otp.go index 02dbdae552..96db142f75 100644 --- a/internal/command/user_human_otp.go +++ b/internal/command/user_human_otp.go @@ -5,6 +5,7 @@ import ( "github.com/zitadel/logging" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" caos_errs "github.com/zitadel/zitadel/internal/errors" @@ -71,11 +72,14 @@ func (c *Commands) AddHumanOTP(ctx context.Context, userID, resourceowner string if accountName == "" { accountName = string(human.EmailAddress) } - key, secret, err := domain.NewOTPKey(c.multifactors.OTP.Issuer, accountName, c.multifactors.OTP.CryptoMFA) + issuer := c.multifactors.OTP.Issuer + if issuer == "" { + issuer = authz.GetInstance(ctx).RequestedDomain() + } + key, secret, err := domain.NewOTPKey(issuer, accountName, c.multifactors.OTP.CryptoMFA) if err != nil { return nil, err } - _, err = c.eventstore.Push(ctx, user.NewHumanOTPAddedEvent(ctx, userAgg, secret)) if err != nil { return nil, err diff --git a/internal/command/user_human_test.go b/internal/command/user_human_test.go index b56ed01c1e..bfbe48ad56 100644 --- a/internal/command/user_human_test.go +++ b/internal/command/user_human_test.go @@ -2,17 +2,19 @@ package command import ( "context" + "errors" "testing" "time" "github.com/golang/mock/gomock" + "github.com/muhlemmer/gu" "github.com/stretchr/testify/assert" "golang.org/x/text/language" "github.com/zitadel/zitadel/internal/command/preparation" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/domain" - "github.com/zitadel/zitadel/internal/errors" + caos_errs "github.com/zitadel/zitadel/internal/errors" "github.com/zitadel/zitadel/internal/eventstore" "github.com/zitadel/zitadel/internal/eventstore/repository" "github.com/zitadel/zitadel/internal/eventstore/v1/models" @@ -29,16 +31,20 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator id.Generator userPasswordAlg crypto.HashAlgorithm codeAlg crypto.EncryptionAlgorithm + newEmailCode func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) } type args struct { ctx context.Context orgID string human *AddHuman secretGenerator crypto.Generator + allowInitMail bool } type res struct { - want *domain.HumanDetails - err func(error) bool + want *domain.ObjectDetails + wantID string + wantEmailCode string + err func(error) bool } userAgg := user.NewAggregate("user1", "org1") @@ -68,9 +74,67 @@ func TestCommandSide_AddHuman(t *testing.T) { Address: "email@test.ch", }, }, + allowInitMail: true, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMA-5Ky74", "Errors.Internal")) + }, + }, + }, + { + name: "user invalid, invalid argument error", + fields: fields{ + eventstore: eventstoreExpect( + t, + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + }, + allowInitMail: true, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")) + }, + }, + }, + { + name: "with id, already exists, precondition error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + newAddHumanEvent("password", true, ""), + ), + ), + ), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + ID: "user1", + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: language.English, + }, + allowInitMail: true, + }, + res: res{ + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-k2unb", "Errors.User.AlreadyExisting")) + }, }, }, { @@ -81,6 +145,7 @@ func TestCommandSide_AddHuman(t *testing.T) { t, expectFilter(), expectFilter(), + expectFilter(), ), }, args: args{ @@ -95,9 +160,12 @@ func TestCommandSide_AddHuman(t *testing.T) { }, PreferredLanguage: language.English, }, + allowInitMail: true, }, res: res{ - err: errors.IsInternal, + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-Ggk9n", "Errors.Internal")) + }, }, }, { @@ -106,6 +174,7 @@ func TestCommandSide_AddHuman(t *testing.T) { idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -134,36 +203,20 @@ func TestCommandSide_AddHuman(t *testing.T) { }, PreferredLanguage: language.English, }, + allowInitMail: true, }, res: res{ - err: errors.IsInternal, - }, - }, - { - name: "user invalid, invalid argument error", - fields: fields{ - idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", - human: &AddHuman{ - Username: "username", - FirstName: "firstname", + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowInternal(nil, "USER-uQ96e", "Errors.Internal")) }, }, - res: res{ - err: errors.IsErrorInvalidArgument, - }, }, { name: "add human (with initial code), ok", fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -237,16 +290,15 @@ func TestCommandSide_AddHuman(t *testing.T) { PreferredLanguage: language.English, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - Sequence: 0, - EventDate: time.Time{}, - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + Sequence: 0, + EventDate: time.Time{}, + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -254,6 +306,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -330,14 +383,174 @@ func TestCommandSide_AddHuman(t *testing.T) { PreferredLanguage: language.English, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", + }, + }, + { + name: "add human (with password and email code custom template), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("password", false, ""), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailCode"), + }, + 1*time.Hour, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}", + false, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newEmailCode: mockEmailCode("emailCode", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + Password: "password", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + URLTemplate: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}", + }, + PreferredLanguage: language.English, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human (with password and return email code), ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + org.NewPasswordComplexityPolicyAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + 1, + false, + false, + false, + false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("password", false, ""), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("emailCode"), + }, + 1*time.Hour, + "", + true, + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + userPasswordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + newEmailCode: mockEmailCode("emailCode", time.Hour), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + Password: "password", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + ReturnCode: true, + }, + PreferredLanguage: language.English, + }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: false, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", + wantEmailCode: "emailCode", }, }, { @@ -345,6 +558,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -400,14 +614,13 @@ func TestCommandSide_AddHuman(t *testing.T) { PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -415,6 +628,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -470,14 +684,13 @@ func TestCommandSide_AddHuman(t *testing.T) { PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -485,6 +698,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -540,14 +754,13 @@ func TestCommandSide_AddHuman(t *testing.T) { PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -555,6 +768,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -594,9 +808,12 @@ func TestCommandSide_AddHuman(t *testing.T) { PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: func(err error) bool { + return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "COMMAND-SFd21", "Errors.User.DomainNotAllowedAsUsername")) + }, }, }, { @@ -604,6 +821,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -687,15 +905,14 @@ func TestCommandSide_AddHuman(t *testing.T) { PasswordChangeRequired: true, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -703,6 +920,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -787,14 +1005,13 @@ func TestCommandSide_AddHuman(t *testing.T) { PreferredLanguage: language.English, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", - }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", }, + wantID: "user1", }, }, { @@ -802,6 +1019,7 @@ func TestCommandSide_AddHuman(t *testing.T) { fields: fields{ eventstore: eventstoreExpect( t, + expectFilter(), expectFilter( eventFromEventPusher( org.NewDomainPolicyAddedEvent(context.Background(), @@ -875,14 +1093,105 @@ func TestCommandSide_AddHuman(t *testing.T) { PreferredLanguage: language.English, }, secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, }, res: res{ - want: &domain.HumanDetails{ - ID: "user1", - ObjectDetails: domain.ObjectDetails{ - ResourceOwner: "org1", + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", + }, + }, + { + name: "add human with metadata, ok", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter(), + expectFilter( + eventFromEventPusher( + org.NewDomainPolicyAddedEvent(context.Background(), + &userAgg.Aggregate, + true, + true, + true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent( + context.Background(), + &instanceAgg.Aggregate, + domain.SecretGeneratorTypeInitCode, + 0, + 1*time.Hour, + true, + true, + true, + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + newAddHumanEvent("", false, ""), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent( + context.Background(), + &userAgg.Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(""), + }, + 1*time.Hour, + ), + ), + eventFromEventPusher( + user.NewMetadataSetEvent( + context.Background(), + &userAgg.Aggregate, + "testKey", + []byte("testValue"), + ), + ), + }, + uniqueConstraintsFromEventConstraint(user.NewAddUsernameUniqueConstraint("username", "org1", true)), + ), + ), + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "user1"), + codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + human: &AddHuman{ + Username: "username", + FirstName: "firstname", + LastName: "lastname", + Email: Email{ + Address: "email@test.ch", + }, + PreferredLanguage: language.English, + Metadata: []*AddMetadataEntry{ + { + Key: "testKey", + Value: []byte("testValue"), + }, }, }, + secretGenerator: GetMockSecretGenerator(t), + allowInitMail: true, + }, + res: res{ + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + wantID: "user1", }, }, } @@ -893,8 +1202,9 @@ func TestCommandSide_AddHuman(t *testing.T) { userPasswordAlg: tt.fields.userPasswordAlg, userEncryption: tt.fields.codeAlg, idGenerator: tt.fields.idGenerator, + newEmailCode: tt.fields.newEmailCode, } - got, err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human) + err := r.AddHuman(tt.args.ctx, tt.args.orgID, tt.args.human, tt.args.allowInitMail) if tt.res.err == nil { if !assert.NoError(t, err) { t.FailNow() @@ -904,7 +1214,9 @@ func TestCommandSide_AddHuman(t *testing.T) { t.Errorf("got wrong err: %v ", err) } if tt.res.err == nil { - assert.Equal(t, tt.res.want, got) + assert.Equal(t, tt.res.want, tt.args.human.Details) + assert.Equal(t, tt.res.wantID, tt.args.human.ID) + assert.Equal(t, tt.res.wantEmailCode, gu.Value(tt.args.human.EmailCode)) } }) } @@ -958,7 +1270,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -985,7 +1297,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -1022,7 +1334,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -1065,7 +1377,7 @@ func TestCommandSide_ImportHuman(t *testing.T) { }, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -1869,7 +2181,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -1899,7 +2211,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -1939,7 +2251,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -1987,7 +2299,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -2056,7 +2368,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsPreconditionFailed, + err: caos_errs.IsPreconditionFailed, }, }, { @@ -2125,7 +2437,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -2211,7 +2523,7 @@ func TestCommandSide_RegisterHuman(t *testing.T) { }, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -3147,7 +3459,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "", }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -3164,7 +3476,7 @@ func TestCommandSide_HumanMFASkip(t *testing.T) { userID: "user1", }, res: res{ - err: errors.IsNotFound, + err: caos_errs.IsNotFound, }, }, { @@ -3261,7 +3573,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{"user1"}, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -3277,7 +3589,7 @@ func TestCommandSide_HumanSignOut(t *testing.T) { userIDs: []string{}, }, res: res{ - err: errors.IsErrorInvalidArgument, + err: caos_errs.IsErrorInvalidArgument, }, }, { @@ -3479,37 +3791,41 @@ func newRegisterHumanEvent(username, password string, changeRequired bool, phone } func TestAddHumanCommand(t *testing.T) { + type fields struct { + idGenerator id.Generator + } type args struct { - a *user.Aggregate - human *AddHuman - passwordAlg crypto.HashAlgorithm - filter preparation.FilterToQueryReducer - codeAlg crypto.EncryptionAlgorithm + human *AddHuman + orgID string + passwordAlg crypto.HashAlgorithm + filter preparation.FilterToQueryReducer + codeAlg crypto.EncryptionAlgorithm + allowInitMail bool } agg := user.NewAggregate("id", "ro") tests := []struct { - name string - args args - want Want + name string + fields fields + args args + want Want }{ { name: "invalid email", args: args{ - a: agg, human: &AddHuman{ Email: Email{ Address: "invalid", }, }, + orgID: "ro", }, want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), + ValidationErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid"), }, }, { name: "invalid first name", args: args{ - a: agg, human: &AddHuman{ Username: "username", PreferredLanguage: language.English, @@ -3517,30 +3833,33 @@ func TestAddHumanCommand(t *testing.T) { Address: "support@zitadel.com", }, }, + orgID: "ro", }, want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), + ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-UCej2", "Errors.User.Profile.FirstNameEmpty"), }, }, { name: "invalid last name", args: args{ - a: agg, human: &AddHuman{ Username: "username", PreferredLanguage: language.English, FirstName: "hurst", Email: Email{Address: "support@zitadel.com"}, }, + orgID: "ro", }, want: Want{ - ValidationErr: errors.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), + ValidationErr: caos_errs.ThrowInvalidArgument(nil, "USER-4hB7d", "Errors.User.Profile.LastNameEmpty"), }, }, { name: "invalid password", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, args: args{ - a: agg, human: &AddHuman{ Email: Email{Address: "support@zitadel.com"}, PreferredLanguage: language.English, @@ -3549,23 +3868,28 @@ func TestAddHumanCommand(t *testing.T) { Password: "short", Username: "username", }, + orgID: "ro", filter: NewMultiFilter().Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewDomainPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id").Aggregate, - true, - true, - true, - ), - }, nil + return []eventstore.Event{}, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ org.NewPasswordComplexityPolicyAddedEvent( - context.Background(), + ctx, &org.NewAggregate("id").Aggregate, 8, true, @@ -3578,13 +3902,15 @@ func TestAddHumanCommand(t *testing.T) { Filter(), }, want: Want{ - CreateErr: errors.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), + CreateErr: caos_errs.ThrowInvalidArgument(nil, "COMMA-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength"), }, }, { name: "correct", + fields: fields{ + idGenerator: id_mock.NewIDGeneratorExpectIDs(t, "id"), + }, args: args{ - a: agg, human: &AddHuman{ Email: Email{Address: "support@zitadel.com", Verified: true}, PreferredLanguage: language.English, @@ -3593,25 +3919,30 @@ func TestAddHumanCommand(t *testing.T) { Password: "password", Username: "username", }, + orgID: "ro", passwordAlg: crypto.CreateMockHashAlg(gomock.NewController(t)), codeAlg: crypto.CreateMockEncryptionAlg(gomock.NewController(t)), filter: NewMultiFilter().Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { - return []eventstore.Event{ - org.NewDomainPolicyAddedEvent( - context.Background(), - &org.NewAggregate("id").Aggregate, - true, - true, - true, - ), - }, nil + return []eventstore.Event{}, nil }). + Append( + func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return []eventstore.Event{ + org.NewDomainPolicyAddedEvent( + ctx, + &org.NewAggregate("id").Aggregate, + true, + true, + true, + ), + }, nil + }). Append( func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { return []eventstore.Event{ org.NewPasswordComplexityPolicyAddedEvent( - context.Background(), + ctx, &org.NewAggregate("id").Aggregate, 2, false, @@ -3654,7 +3985,25 @@ func TestAddHumanCommand(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - AssertValidation(t, context.Background(), AddHumanCommand(tt.args.a, tt.args.human, tt.args.passwordAlg, tt.args.codeAlg), tt.args.filter, tt.want) + c := &Commands{ + idGenerator: tt.fields.idGenerator, + } + AssertValidation(t, context.Background(), c.AddHumanCommand(tt.args.human, tt.args.orgID, tt.args.passwordAlg, tt.args.codeAlg, tt.args.allowInitMail), tt.args.filter, tt.want) }) } } + +func mockEmailCode(code string, exp time.Duration) func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) { + return func(ctx context.Context, filter preparation.FilterToQueryReducer, alg crypto.EncryptionAlgorithm) (*CryptoCodeWithExpiry, error) { + return &CryptoCodeWithExpiry{ + Crypted: &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte(code), + }, + Plain: code, + Expiry: exp, + }, nil + } +} diff --git a/internal/command/user_v2_email.go b/internal/command/user_v2_email.go new file mode 100644 index 0000000000..d636b21a71 --- /dev/null +++ b/internal/command/user_v2_email.go @@ -0,0 +1,208 @@ +package command + +import ( + "context" + "io" + + "github.com/zitadel/logging" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/user" +) + +// ChangeUserEmail sets a user's email address, generates a code +// and triggers a notification e-mail with the default confirmation URL format. +func (c *Commands) ChangeUserEmail(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, "") +} + +// ChangeUserEmailURLTemplate sets a user's email address, generates a code +// and triggers a notification e-mail with the confirmation URL rendered from the passed urlTmpl. +// urlTmpl must be a valid [tmpl.Template]. +func (c *Commands) ChangeUserEmailURLTemplate(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, urlTmpl string) (*domain.Email, error) { + if err := domain.RenderConfirmURLTemplate(io.Discard, urlTmpl, userID, "code", "orgID"); err != nil { + return nil, err + } + return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, false, urlTmpl) +} + +// ChangeUserEmailReturnCode sets a user's email address, generates a code and does not send a notification email. +// The generated plain text code will be set in the returned Email object. +func (c *Commands) ChangeUserEmailReturnCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm) (*domain.Email, error) { + return c.changeUserEmailWithCode(ctx, userID, resourceOwner, email, alg, true, "") +} + +// ChangeUserEmailVerified sets a user's email address and marks it is verified. +// No code is generated and no confirmation e-mail is send. +func (c *Commands) ChangeUserEmailVerified(ctx context.Context, userID, resourceOwner, email string) (*domain.Email, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, false); err != nil { + return nil, err + } + if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil { + return nil, err + } + cmd.SetVerified(ctx) + return cmd.Push(ctx) +} + +func (c *Commands) changeUserEmailWithCode(ctx context.Context, userID, resourceOwner, email string, alg crypto.EncryptionAlgorithm, returnCode bool, urlTmpl string) (*domain.Email, error) { + config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode) + if err != nil { + return nil, err + } + gen := crypto.NewEncryptionGenerator(*config, alg) + return c.changeUserEmailWithGenerator(ctx, userID, resourceOwner, email, gen, returnCode, urlTmpl) +} + +// changeUserEmailWithGenerator set a user's email address. +// returnCode controls if the plain text version of the code will be set in the return object. +// When the plain text code is returned, no notification e-mail will be send to the user. +// urlTmpl allows changing the target URL that is used by the e-mail and should be a validated Go template, if used. +func (c *Commands) changeUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, email string, gen crypto.Generator, returnCode bool, urlTmpl string) (*domain.Email, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + if err = c.checkPermission(ctx, permissionUserWrite, cmd.aggregate.ResourceOwner, userID, true); err != nil { + return nil, err + } + if err = cmd.Change(ctx, domain.EmailAddress(email)); err != nil { + return nil, err + } + if err = cmd.AddGeneratedCode(ctx, gen, urlTmpl, returnCode); err != nil { + return nil, err + } + return cmd.Push(ctx) +} + +func (c *Commands) VerifyUserEmail(ctx context.Context, userID, resourceOwner, code string, alg crypto.EncryptionAlgorithm) (*domain.ObjectDetails, error) { + config, err := secretGeneratorConfig(ctx, c.eventstore.Filter, domain.SecretGeneratorTypeVerifyEmailCode) + if err != nil { + return nil, err + } + gen := crypto.NewEncryptionGenerator(*config, alg) + return c.verifyUserEmailWithGenerator(ctx, userID, resourceOwner, code, gen) +} + +func (c *Commands) verifyUserEmailWithGenerator(ctx context.Context, userID, resourceOwner, code string, gen crypto.Generator) (*domain.ObjectDetails, error) { + cmd, err := c.NewUserEmailEvents(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + err = cmd.VerifyCode(ctx, code, gen) + if err != nil { + return nil, err + } + if _, err = cmd.Push(ctx); err != nil { + return nil, err + } + return writeModelToObjectDetails(&cmd.model.WriteModel), nil +} + +// UserEmailEvents allows step-by-step additions of events, +// operating on the Human Email Model. +type UserEmailEvents struct { + eventstore *eventstore.Eventstore + aggregate *eventstore.Aggregate + events []eventstore.Command + model *HumanEmailWriteModel + + plainCode *string +} + +// NewUserEmailEvents constructs a UserEmailEvents with a Human Email Write Model, +// filtered by userID and resourceOwner. +// If a model cannot be found, or it's state is invalid and error is returned. +func (c *Commands) NewUserEmailEvents(ctx context.Context, userID, resourceOwner string) (*UserEmailEvents, error) { + if userID == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing") + } + + model, err := c.emailWriteModel(ctx, userID, resourceOwner) + if err != nil { + return nil, err + } + if model.UserState == domain.UserStateUnspecified || model.UserState == domain.UserStateDeleted { + return nil, caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Email.NotFound") + } + if model.UserState == domain.UserStateInitial { + return nil, caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised") + } + return &UserEmailEvents{ + eventstore: c.eventstore, + aggregate: UserAggregateFromWriteModel(&model.WriteModel), + model: model, + }, nil +} + +// Change sets a new email address. +// The generated event unsets any previously generated code and verified flag. +func (c *UserEmailEvents) Change(ctx context.Context, email domain.EmailAddress) error { + if err := email.Validate(); err != nil { + return err + } + event, hasChanged := c.model.NewChangedEvent(ctx, c.aggregate, email) + if !hasChanged { + return caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged") + } + c.events = append(c.events, event) + return nil +} + +// SetVerified sets the email address to verified. +func (c *UserEmailEvents) SetVerified(ctx context.Context) { + c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate)) +} + +// AddGeneratedCode generates a new encrypted code and sets it to the email address. +// When returnCode a plain text of the code will be returned from Push. +func (c *UserEmailEvents) AddGeneratedCode(ctx context.Context, gen crypto.Generator, urlTmpl string, returnCode bool) error { + value, plain, err := crypto.NewCode(gen) + if err != nil { + return err + } + + c.events = append(c.events, user.NewHumanEmailCodeAddedEventV2(ctx, c.aggregate, value, gen.Expiry(), urlTmpl, returnCode)) + if returnCode { + c.plainCode = &plain + } + return nil +} + +func (c *UserEmailEvents) VerifyCode(ctx context.Context, code string, gen crypto.Generator) error { + if code == "" { + return caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty") + } + + err := crypto.VerifyCode(c.model.CodeCreationDate, c.model.CodeExpiry, c.model.Code, code, gen) + if err == nil { + c.events = append(c.events, user.NewHumanEmailVerifiedEvent(ctx, c.aggregate)) + return nil + } + _, err = c.eventstore.Push(ctx, user.NewHumanEmailVerificationFailedEvent(ctx, c.aggregate)) + logging.WithFields("id", "COMMAND-Zoo6b", "userID", c.aggregate.ID).OnError(err).Error("NewHumanEmailVerificationFailedEvent push failed") + return caos_errs.ThrowInvalidArgument(err, "COMMAND-eis9R", "Errors.User.Code.Invalid") +} + +// Push all events to the eventstore and Reduce them into the Model. +func (c *UserEmailEvents) Push(ctx context.Context) (*domain.Email, error) { + pushedEvents, err := c.eventstore.Push(ctx, c.events...) + if err != nil { + return nil, err + } + err = AppendAndReduce(c.model, pushedEvents...) + if err != nil { + return nil, err + } + email := writeModelToEmail(c.model) + email.PlainCode = c.plainCode + + return email, nil +} diff --git a/internal/command/user_v2_email_test.go b/internal/command/user_v2_email_test.go new file mode 100644 index 0000000000..9989229efd --- /dev/null +++ b/internal/command/user_v2_email_test.go @@ -0,0 +1,1315 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/language" + + "github.com/zitadel/zitadel/internal/crypto" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/repository" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/repository/instance" + "github.com/zitadel/zitadel/internal/repository/user" +) + +func TestCommands_ChangeUserEmail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission permissionCheck + } + type args struct { + userID string + resourceOwner string + email string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "", + }, + wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "missing email", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), + }, + { + name: "not changed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + }, + wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ChangeUserEmail(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_changeUserEmailWithGenerator + }) + } +} + +func TestCommands_ChangeUserEmailURLTemplate(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission permissionCheck + } + type args struct { + userID string + resourceOwner string + email string + urlTmpl string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "invalid template", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + urlTmpl: "{{", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"), + }, + { + name: "permission missing", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "not changed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ChangeUserEmailURLTemplate(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t)), tt.args.urlTmpl) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_changeUserEmailWithGenerator + }) + } +} + +func TestCommands_ChangeUserEmailReturnCode(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission permissionCheck + } + type args struct { + userID string + resourceOwner string + email string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + }, + wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "missing email", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + _, err := c.ChangeUserEmailReturnCode(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_changeUserEmailWithGenerator + }) + } +} + +func TestCommands_ChangeUserEmailVerified(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission permissionCheck + } + type args struct { + userID string + resourceOwner string + email string + } + tests := []struct { + name string + fields fields + args args + want *domain.Email + wantErr error + }{ + { + name: "missing userID", + fields: fields{ + eventstore: eventstoreExpect(t), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "", + resourceOwner: "org1", + email: "email@test.ch", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + }, + wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "missing email", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), + }, + { + name: "email changed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + got, err := c.ChangeUserEmailVerified(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, got, tt.want) + }) + } +} + +func TestCommands_changeUserEmailWithGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + checkPermission permissionCheck + } + type args struct { + userID string + resourceOwner string + email string + returnCode bool + urlTmpl string + } + tests := []struct { + name string + fields fields + args args + want *domain.Email + wantErr error + }{ + { + name: "missing user", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "", + resourceOwner: "org1", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "missing permission", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckNotAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", + }, + wantErr: caos_errs.ThrowPermissionDenied(nil, "AUTHZ-HKJD33", "Errors.PermissionDenied"), + }, + { + name: "missing email", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "", + returnCode: false, + urlTmpl: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty"), + }, + { + name: "not changed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email@test.ch", + returnCode: false, + urlTmpl: "", + }, + wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-Uch5e", "Errors.User.Email.NotChanged"), + }, + { + name: "email changed", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + }, + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + returnCode: false, + urlTmpl: "", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: false, + }, + }, + { + name: "email changed, return code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", true, + ), + ), + }, + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + returnCode: true, + urlTmpl: "", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: false, + PlainCode: gu.Ptr("a"), + }, + }, + { + name: "email changed, URL template", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailChangedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "email-changed@test.ch", + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", false, + ), + ), + }, + ), + ), + checkPermission: newMockPermissionCheckAllowed(), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + email: "email-changed@test.ch", + returnCode: false, + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + want: &domain.Email{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "user1", + ResourceOwner: "org1", + }, + EmailAddress: "email-changed@test.ch", + IsEmailVerified: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + checkPermission: tt.fields.checkPermission, + } + got, err := c.changeUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.email, GetMockSecretGenerator(t), tt.args.returnCode, tt.args.urlTmpl) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, got, tt.want) + }) + } +} + +func TestCommands_VerifyUserEmail(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + userID string + resourceOwner string + code string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing userID", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + ), + }, + args: args{ + userID: "", + resourceOwner: "org1", + code: "a", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "missing code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + code: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty"), + }, + { + name: "wrong code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewSecretGeneratorAddedEvent(context.Background(), + &instance.NewAggregate("inst1").Aggregate, + domain.SecretGeneratorTypeVerifyEmailCode, + 12, time.Minute, true, true, true, true, + ), + ), + ), + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailVerificationFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + code: "wrong", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-eis9R", "Errors.User.Code.Invalid"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + _, err := c.VerifyUserEmail(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.code, crypto.CreateMockEncryptionAlg(gomock.NewController(t))) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_verifyUserEmailWithGenerator + }) + } +} + +func TestCommands_verifyUserEmailWithGenerator(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + userID string + resourceOwner string + code string + } + tests := []struct { + name string + fields fields + args args + want *domain.ObjectDetails + wantErr error + }{ + { + name: "missing userID", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "", + resourceOwner: "org1", + code: "a", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "missing code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + code: "", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-Fia4a", "Errors.User.Code.Empty"), + }, + { + name: "good code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailVerificationFailedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + code: "wrong", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-eis9R", "Errors.User.Code.Invalid"), + }, + { + name: "wrong code", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusherWithCreationDateNow( + user.NewHumanEmailCodeAddedEventV2(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + &crypto.CryptoValue{ + CryptoType: crypto.TypeEncryption, + Algorithm: "enc", + KeyID: "id", + Crypted: []byte("a"), + }, + time.Hour*1, + "", false, + ), + ), + ), + expectPush( + []*repository.Event{ + eventFromEventPusher( + user.NewHumanEmailVerifiedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + ), + ), + }, + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + code: "a", + }, + want: &domain.ObjectDetails{ + ResourceOwner: "org1", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + got, err := c.verifyUserEmailWithGenerator(context.Background(), tt.args.userID, tt.args.resourceOwner, tt.args.code, GetMockSecretGenerator(t)) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, got, tt.want) + }) + } +} + +func TestCommands_NewUserEmailEvents(t *testing.T) { + type fields struct { + eventstore *eventstore.Eventstore + } + type args struct { + userID string + resourceOwner string + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "missing userID", + fields: fields{ + eventstore: eventstoreExpect(t), + }, + args: args{ + userID: "", + resourceOwner: "org1", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "COMMAND-0Gzs3", "Errors.User.Email.IDMissing"), + }, + { + name: "not found", + fields: fields{ + eventstore: eventstoreExpect(t, expectFilter()), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + wantErr: caos_errs.ThrowNotFound(nil, "COMMAND-ieJ2e", "Errors.User.Email.NotFound"), + }, + { + name: "user not initialized", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.German, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + eventFromEventPusher( + user.NewHumanInitialCodeAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + nil, time.Hour*1, + ), + ), + ), + ), + }, + args: args{ + userID: "user1", + resourceOwner: "org1", + }, + wantErr: caos_errs.ThrowPreconditionFailed(nil, "COMMAND-uz0Uu", "Errors.User.NotInitialised"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &Commands{ + eventstore: tt.fields.eventstore, + } + _, err := c.NewUserEmailEvents(context.Background(), tt.args.userID, tt.args.resourceOwner) + require.ErrorIs(t, err, tt.wantErr) + // successful cases are tested in TestCommands_changeUserEmailWithGenerator + }) + } +} diff --git a/internal/domain/application_oidc.go b/internal/domain/application_oidc.go index ad08b18d7a..e0ae783e8e 100644 --- a/internal/domain/application_oidc.go +++ b/internal/domain/application_oidc.go @@ -90,6 +90,7 @@ const ( OIDCGrantTypeAuthorizationCode OIDCGrantType = iota OIDCGrantTypeImplicit OIDCGrantTypeRefreshToken + OIDCGrantTypeDeviceCode ) type OIDCApplicationType int32 diff --git a/internal/domain/auth_request.go b/internal/domain/auth_request.go index 84ca2b9ab8..86cd0575f6 100644 --- a/internal/domain/auth_request.go +++ b/internal/domain/auth_request.go @@ -122,6 +122,8 @@ func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) { return &AuthRequest{Request: &AuthRequestOIDC{}}, nil case AuthRequestTypeSAML: return &AuthRequest{Request: &AuthRequestSAML{}}, nil + case AuthRequestTypeDevice: + return &AuthRequest{Request: &AuthRequestDevice{}}, nil } return nil, errors.ThrowInvalidArgument(nil, "DOMAIN-ds2kl", "invalid request type") } @@ -184,3 +186,12 @@ func (a *AuthRequest) GetScopeOrgID() string { } return "" } + +func (a *AuthRequest) Done() bool { + for _, step := range a.PossibleSteps { + if step.Type() == NextStepRedirectToCallback { + return true + } + } + return false +} diff --git a/internal/domain/device_auth.go b/internal/domain/device_auth.go new file mode 100644 index 0000000000..da076663aa --- /dev/null +++ b/internal/domain/device_auth.go @@ -0,0 +1,83 @@ +package domain + +import ( + "strconv" + "time" + + "github.com/zitadel/zitadel/internal/eventstore/v1/models" +) + +// DeviceAuth describes a Device Authorization request. +// It is used as input and output model in the command and query packages. +type DeviceAuth struct { + models.ObjectRoot + + ClientID string + DeviceCode string + UserCode string + Expires time.Time + Scopes []string + Subject string + State DeviceAuthState +} + +// DeviceAuthState describes the step the +// the device authorization process is in. +// We generate the Stringer implemntation for pretier +// log output. +// +//go:generate stringer -type=DeviceAuthState -linecomment +type DeviceAuthState uint + +const ( + DeviceAuthStateUndefined DeviceAuthState = iota // undefined + DeviceAuthStateInitiated // initiated + DeviceAuthStateApproved // approved + DeviceAuthStateDenied // denied + DeviceAuthStateExpired // expired + DeviceAuthStateRemoved // removed +) + +// Exists returns true when not Undefined and +// any status lower than Removed. +func (s DeviceAuthState) Exists() bool { + return s > DeviceAuthStateUndefined && s < DeviceAuthStateRemoved +} + +// Done returns true when DeviceAuthState is Approved. +// This implements the OIDC interface requirement of "Done" +func (s DeviceAuthState) Done() bool { + return s == DeviceAuthStateApproved +} + +// Denied returns true when DeviceAuthState is Denied, Expired or Removed. +// This implements the OIDC interface requirement of "Denied". +func (s DeviceAuthState) Denied() bool { + return s >= DeviceAuthStateDenied +} + +func (s DeviceAuthState) GoString() string { + return strconv.Itoa(int(s)) +} + +// DeviceAuthCanceled is a subset of DeviceAuthState, allowed to +// be used in the deviceauth.CanceledEvent. +// The string type is used to make the eventstore more readable +// on the reason of cancelation. +type DeviceAuthCanceled string + +const ( + DeviceAuthCanceledDenied = "denied" + DeviceAuthCanceledExpired = "expired" +) + +func (c DeviceAuthCanceled) State() DeviceAuthState { + switch c { + case DeviceAuthCanceledDenied: + return DeviceAuthStateDenied + case DeviceAuthCanceledExpired: + return DeviceAuthStateExpired + default: + return DeviceAuthStateUndefined + } +} diff --git a/internal/domain/device_auth_test.go b/internal/domain/device_auth_test.go new file mode 100644 index 0000000000..c3fcf359da --- /dev/null +++ b/internal/domain/device_auth_test.go @@ -0,0 +1,158 @@ +package domain + +import ( + "testing" +) + +func TestDeviceAuthState_Exists(t *testing.T) { + tests := []struct { + s DeviceAuthState + want bool + }{ + { + s: DeviceAuthStateUndefined, + want: false, + }, + { + s: DeviceAuthStateInitiated, + want: true, + }, + { + s: DeviceAuthStateApproved, + want: true, + }, + { + s: DeviceAuthStateDenied, + want: true, + }, + { + s: DeviceAuthStateExpired, + want: true, + }, + { + s: DeviceAuthStateRemoved, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.s.String(), func(t *testing.T) { + if got := tt.s.Exists(); got != tt.want { + t.Errorf("DeviceAuthState.Exists() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDeviceAuthState_Done(t *testing.T) { + tests := []struct { + s DeviceAuthState + want bool + }{ + { + s: DeviceAuthStateUndefined, + want: false, + }, + { + s: DeviceAuthStateInitiated, + want: false, + }, + { + s: DeviceAuthStateApproved, + want: true, + }, + { + s: DeviceAuthStateDenied, + want: false, + }, + { + s: DeviceAuthStateExpired, + want: false, + }, + { + s: DeviceAuthStateRemoved, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.s.String(), func(t *testing.T) { + if got := tt.s.Done(); got != tt.want { + t.Errorf("DeviceAuthState.Done() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDeviceAuthState_Denied(t *testing.T) { + tests := []struct { + name string + s DeviceAuthState + want bool + }{ + { + s: DeviceAuthStateUndefined, + want: false, + }, + { + s: DeviceAuthStateInitiated, + want: false, + }, + { + s: DeviceAuthStateApproved, + want: false, + }, + { + s: DeviceAuthStateDenied, + want: true, + }, + { + s: DeviceAuthStateExpired, + want: true, + }, + { + s: DeviceAuthStateRemoved, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.s.Denied(); got != tt.want { + t.Errorf("DeviceAuthState.Denied() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDeviceAuthCanceled_State(t *testing.T) { + tests := []struct { + name string + c DeviceAuthCanceled + want DeviceAuthState + }{ + { + name: "empty", + want: DeviceAuthStateUndefined, + }, + { + name: "invalid", + c: "foo", + want: DeviceAuthStateUndefined, + }, + { + name: "denied", + c: DeviceAuthCanceledDenied, + want: DeviceAuthStateDenied, + }, + { + name: "expired", + c: DeviceAuthCanceledExpired, + want: DeviceAuthStateExpired, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.c.State(); got != tt.want { + t.Errorf("DeviceAuthCanceled.State() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/domain/deviceauthstate_string.go b/internal/domain/deviceauthstate_string.go new file mode 100644 index 0000000000..b47a6bc7e8 --- /dev/null +++ b/internal/domain/deviceauthstate_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type=DeviceAuthState -linecomment"; DO NOT EDIT. + +package domain + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[DeviceAuthStateUndefined-0] + _ = x[DeviceAuthStateInitiated-1] + _ = x[DeviceAuthStateApproved-2] + _ = x[DeviceAuthStateDenied-3] + _ = x[DeviceAuthStateExpired-4] + _ = x[DeviceAuthStateRemoved-5] +} + +const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpiredremoved" + +var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 46} + +func (i DeviceAuthState) String() string { + if i >= DeviceAuthState(len(_DeviceAuthState_index)-1) { + return "DeviceAuthState(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _DeviceAuthState_name[_DeviceAuthState_index[i]:_DeviceAuthState_index[i+1]] +} diff --git a/internal/domain/human.go b/internal/domain/human.go index 05e56ace67..619eaf4807 100644 --- a/internal/domain/human.go +++ b/internal/domain/human.go @@ -10,11 +10,6 @@ import ( es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) -type HumanDetails struct { - ID string - ObjectDetails -} - type Human struct { es_models.ObjectRoot diff --git a/internal/domain/human_email.go b/internal/domain/human_email.go index 18bf0440d0..578c9fe220 100644 --- a/internal/domain/human_email.go +++ b/internal/domain/human_email.go @@ -1,12 +1,15 @@ package domain import ( + "io" "regexp" "strings" + "text/template" "time" "github.com/zitadel/zitadel/internal/crypto" "github.com/zitadel/zitadel/internal/errors" + caos_errs "github.com/zitadel/zitadel/internal/errors" es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models" ) @@ -35,6 +38,8 @@ type Email struct { EmailAddress EmailAddress IsEmailVerified bool + // PlainCode is set by the command and can be used to return it to the caller (API) + PlainCode *string } type EmailCode struct { @@ -51,13 +56,36 @@ func (e *Email) Validate() error { return e.EmailAddress.Validate() } -func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, error) { - emailCodeCrypto, _, err := crypto.NewCode(emailGenerator) +func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, string, error) { + emailCodeCrypto, code, err := crypto.NewCode(emailGenerator) if err != nil { - return nil, err + return nil, "", err } return &EmailCode{ Code: emailCodeCrypto, Expiry: emailGenerator.Expiry(), - }, nil + }, code, nil +} + +type ConfirmURLData struct { + UserID string + Code string + OrgID string +} + +// RenderConfirmURLTemplate parses and renders tmplStr. +// userID, code and orgID are passed into the [ConfirmURLData]. +// "%s%s?userID=%s&code=%s&orgID=%s" +func RenderConfirmURLTemplate(w io.Writer, tmplStr, userID, code, orgID string) error { + tmpl, err := template.New("").Parse(tmplStr) + if err != nil { + return caos_errs.ThrowInvalidArgument(err, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate") + } + + data := &ConfirmURLData{userID, code, orgID} + if err = tmpl.Execute(w, data); err != nil { + return caos_errs.ThrowInvalidArgument(err, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate") + } + + return nil } diff --git a/internal/domain/human_email_test.go b/internal/domain/human_email_test.go index c516ade47d..f77ba6d5c2 100644 --- a/internal/domain/human_email_test.go +++ b/internal/domain/human_email_test.go @@ -1,7 +1,13 @@ package domain import ( + "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + caos_errs "github.com/zitadel/zitadel/internal/errors" ) func TestEmailValid(t *testing.T) { @@ -72,3 +78,57 @@ func TestEmailValid(t *testing.T) { }) } } + +func TestRenderConfirmURLTemplate(t *testing.T) { + type args struct { + tmplStr string + userID string + code string + orgID string + } + tests := []struct { + name string + args args + want string + wantErr error + }{ + { + name: "invalid template", + args: args{ + tmplStr: "{{", + userID: "user1", + code: "123", + orgID: "org1", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"), + }, + { + name: "execution error", + args: args{ + tmplStr: "{{.Foo}}", + userID: "user1", + code: "123", + orgID: "org1", + }, + wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ohSi5", "Errors.User.Email.InvalidURLTemplate"), + }, + { + name: "success", + args: args{ + tmplStr: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + userID: "user1", + code: "123", + orgID: "org1", + }, + want: "https://example.com/email/verify?userID=user1&code=123&orgID=org1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var w strings.Builder + err := RenderConfirmURLTemplate(&w, tt.args.tmplStr, tt.args.userID, tt.args.code, tt.args.orgID) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, w.String()) + }) + } +} diff --git a/internal/domain/request.go b/internal/domain/request.go index d76ab2cf4f..7f91463921 100644 --- a/internal/domain/request.go +++ b/internal/domain/request.go @@ -22,6 +22,7 @@ type AuthRequestType int32 const ( AuthRequestTypeOIDC AuthRequestType = iota AuthRequestTypeSAML + AuthRequestTypeDevice ) type AuthRequestOIDC struct { @@ -56,3 +57,18 @@ func (a *AuthRequestSAML) Type() AuthRequestType { func (a *AuthRequestSAML) IsValid() bool { return true } + +type AuthRequestDevice struct { + ID string + DeviceCode string + UserCode string + Scopes []string +} + +func (*AuthRequestDevice) Type() AuthRequestType { + return AuthRequestTypeDevice +} + +func (a *AuthRequestDevice) IsValid() bool { + return a.DeviceCode != "" && a.UserCode != "" && len(a.Scopes) > 0 +} diff --git a/internal/eventstore/config.go b/internal/eventstore/config.go index c9d00da267..21011dea67 100644 --- a/internal/eventstore/config.go +++ b/internal/eventstore/config.go @@ -9,8 +9,9 @@ import ( ) type Config struct { - PushTimeout time.Duration - Client *database.DB + PushTimeout time.Duration + Client *database.DB + AllowOrderByCreationDate bool repo repository.Repository } @@ -20,6 +21,6 @@ func TestConfig(repo repository.Repository) *Config { } func Start(config *Config) (*Eventstore, error) { - config.repo = z_sql.NewCRDB(config.Client) + config.repo = z_sql.NewCRDB(config.Client, config.AllowOrderByCreationDate) return NewEventstore(config), nil } diff --git a/internal/eventstore/eventstore.go b/internal/eventstore/eventstore.go index 6e9e47f8e4..216a29bdee 100644 --- a/internal/eventstore/eventstore.go +++ b/internal/eventstore/eventstore.go @@ -304,3 +304,21 @@ func uniqueConstraintActionToRepository(action UniqueConstraintAction) repositor return repository.UniqueConstraintAdd } } + +type BaseEventSetter[T any] interface { + Event + SetBaseEvent(*BaseEvent) + *T +} + +func GenericEventMapper[T any, PT BaseEventSetter[T]](event *repository.Event) (Event, error) { + e := PT(new(T)) + e.SetBaseEvent(BaseEventFromRepo(event)) + + err := json.Unmarshal(event.Data, e) + if err != nil { + return nil, errors.ThrowInternal(err, "V2-Thai6", "unable to unmarshal event") + } + + return e, nil +} diff --git a/internal/eventstore/handler/crdb/handler_stmt.go b/internal/eventstore/handler/crdb/handler_stmt.go index 65eb99426a..8f6d9481f0 100644 --- a/internal/eventstore/handler/crdb/handler_stmt.go +++ b/internal/eventstore/handler/crdb/handler_stmt.go @@ -267,6 +267,7 @@ func (h *StatementHandler) executeStmt(tx *sql.Tx, stmt *handler.Statement) erro } err = stmt.Execute(tx, h.ProjectionName) if err != nil { + logging.WithError(err).Error() _, rollbackErr := tx.Exec("ROLLBACK TO SAVEPOINT push_stmt") if rollbackErr != nil { return errors.ThrowInternal(rollbackErr, "CRDB-zzp3P", "rollback to savepoint failed") diff --git a/internal/eventstore/handler/crdb/init.go b/internal/eventstore/handler/crdb/init.go index 9ca9c34a9d..420b11731a 100644 --- a/internal/eventstore/handler/crdb/init.go +++ b/internal/eventstore/handler/crdb/init.go @@ -377,6 +377,8 @@ func defaultValue(value interface{}) string { switch v := value.(type) { case string: return "'" + v + "'" + case fmt.Stringer: + return fmt.Sprintf("%#v", v) default: return fmt.Sprintf("%v", v) } diff --git a/internal/eventstore/handler/crdb/init_test.go b/internal/eventstore/handler/crdb/init_test.go new file mode 100644 index 0000000000..1e7e6bd823 --- /dev/null +++ b/internal/eventstore/handler/crdb/init_test.go @@ -0,0 +1,49 @@ +package crdb + +import "testing" + +func Test_defaultValue(t *testing.T) { + type args struct { + value interface{} + } + tests := []struct { + name string + args args + want string + }{ + { + name: "string", + args: args{ + value: "asdf", + }, + want: "'asdf'", + }, + { + name: "primitive non string", + args: args{ + value: 1, + }, + want: "1", + }, + { + name: "stringer", + args: args{ + value: testStringer(0), + }, + want: "0", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := defaultValue(tt.args.value); got != tt.want { + t.Errorf("defaultValue() = %v, want %v", got, tt.want) + } + }) + } +} + +type testStringer int + +func (t testStringer) String() string { + return "0529958243" +} diff --git a/internal/eventstore/handler/handler_projection.go b/internal/eventstore/handler/handler_projection.go index 2b4160586f..808da1dbb8 100644 --- a/internal/eventstore/handler/handler_projection.go +++ b/internal/eventstore/handler/handler_projection.go @@ -223,7 +223,7 @@ func (h *ProjectionHandler) schedule(ctx context.Context) { errs := h.lock(lockCtx, h.requeueAfter, "system") if err, ok := <-errs; err != nil || !ok { cancelLock() - logging.WithFields("projection", h.ProjectionName).OnError(err).Warn("initial lock failed for first schedule") + logging.WithFields("projection", h.ProjectionName).OnError(err).Debug("initial lock failed for first schedule") h.triggerProjection.Reset(h.requeueAfter) continue } @@ -253,7 +253,7 @@ func (h *ProjectionHandler) schedule(ctx context.Context) { //wait until projection is locked if err, ok := <-errs; err != nil || !ok { cancelInstanceLock() - logging.WithFields("projection", h.ProjectionName).OnError(err).Warn("initial lock failed") + logging.WithFields("projection", h.ProjectionName).OnError(err).Debug("initial lock failed") failed = true continue } diff --git a/internal/eventstore/repository/sql/crdb.go b/internal/eventstore/repository/sql/crdb.go index 6400776251..f27936de4c 100644 --- a/internal/eventstore/repository/sql/crdb.go +++ b/internal/eventstore/repository/sql/crdb.go @@ -20,7 +20,7 @@ import ( const ( //as soon as stored procedures are possible in crdb - // we could move the code to migrations and coll the procedure + // we could move the code to migrations and call the procedure // traking issue: https://github.com/cockroachdb/cockroach/issues/17511 // //previous_data selects the needed data of the latest event of the aggregate @@ -99,10 +99,11 @@ const ( type CRDB struct { *database.DB + AllowOrderByCreationDate bool } -func NewCRDB(client *database.DB) *CRDB { - return &CRDB{client} +func NewCRDB(client *database.DB, allowOrderByCreationDate bool) *CRDB { + return &CRDB{client, allowOrderByCreationDate} } func (db *CRDB) Health(ctx context.Context) error { return db.Ping() } @@ -254,11 +255,19 @@ func (db *CRDB) db() *sql.DB { } func (db *CRDB) orderByEventSequence(desc bool) string { - if desc { - return " ORDER BY creation_date DESC, event_sequence DESC" + if db.AllowOrderByCreationDate { + if desc { + return " ORDER BY creation_date DESC, event_sequence DESC" + } + + return " ORDER BY creation_date, event_sequence" } - return " ORDER BY creation_date, event_sequence" + if desc { + return " ORDER BY event_sequence DESC" + } + + return " ORDER BY event_sequence" } func (db *CRDB) eventQuery() string { diff --git a/internal/eventstore/repository/sql/query_test.go b/internal/eventstore/repository/sql/query_test.go index 9560ff0918..af9aa9860c 100644 --- a/internal/eventstore/repository/sql/query_test.go +++ b/internal/eventstore/repository/sql/query_test.go @@ -542,6 +542,7 @@ func Test_query_events_with_crdb(t *testing.T) { DB: tt.fields.client, Database: new(testDB), }, + AllowOrderByCreationDate: true, } // setup initial data for query @@ -820,9 +821,12 @@ func Test_query_events_mocked(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - crdb := &CRDB{DB: &database.DB{ - Database: new(testDB), - }} + crdb := &CRDB{ + DB: &database.DB{ + Database: new(testDB), + }, + AllowOrderByCreationDate: true, + } if tt.fields.mock != nil { crdb.DB.DB = tt.fields.mock.client } diff --git a/internal/eventstore/v1/eventstore.go b/internal/eventstore/v1/eventstore.go index 0fc311f2e4..49e0910c73 100644 --- a/internal/eventstore/v1/eventstore.go +++ b/internal/eventstore/v1/eventstore.go @@ -22,9 +22,9 @@ type eventstore struct { repo repository.Repository } -func Start(db *database.DB) (Eventstore, error) { +func Start(db *database.DB, allowOrderByCreationDate bool) (Eventstore, error) { return &eventstore{ - repo: z_sql.Start(db), + repo: z_sql.Start(db, allowOrderByCreationDate), }, nil } diff --git a/internal/eventstore/v1/internal/repository/sql/config.go b/internal/eventstore/v1/internal/repository/sql/config.go index bef2df0de2..5ea34e6e40 100644 --- a/internal/eventstore/v1/internal/repository/sql/config.go +++ b/internal/eventstore/v1/internal/repository/sql/config.go @@ -4,8 +4,9 @@ import ( "github.com/zitadel/zitadel/internal/database" ) -func Start(client *database.DB) *SQL { +func Start(client *database.DB, allowOrderByCreationDate bool) *SQL { return &SQL{ - client: client, + client: client, + allowOrderByCreationDate: allowOrderByCreationDate, } } diff --git a/internal/eventstore/v1/internal/repository/sql/filter.go b/internal/eventstore/v1/internal/repository/sql/filter.go index 5674972de6..ab67730b59 100644 --- a/internal/eventstore/v1/internal/repository/sql/filter.go +++ b/internal/eventstore/v1/internal/repository/sql/filter.go @@ -21,11 +21,11 @@ func (db *SQL) Filter(ctx context.Context, searchQuery *es_models.SearchQueryFac if !searchQuery.InstanceFiltered { logging.WithFields("stack", string(debug.Stack())).Warn("instanceid not filtered") } - return filter(ctx, db.client, searchQuery) + return db.filter(ctx, db.client, searchQuery) } -func filter(ctx context.Context, db *database.DB, searchQuery *es_models.SearchQueryFactory) (events []*es_models.Event, err error) { - query, limit, values, rowScanner := buildQuery(ctx, db, searchQuery) +func (sql *SQL) filter(ctx context.Context, db *database.DB, searchQuery *es_models.SearchQueryFactory) (events []*es_models.Event, err error) { + query, limit, values, rowScanner := sql.buildQuery(ctx, db, searchQuery) if query == "" { return nil, errors.ThrowInvalidArgument(nil, "SQL-rWeBw", "invalid query factory") } @@ -53,7 +53,7 @@ func filter(ctx context.Context, db *database.DB, searchQuery *es_models.SearchQ } func (db *SQL) LatestSequence(ctx context.Context, queryFactory *es_models.SearchQueryFactory) (uint64, error) { - query, _, values, rowScanner := buildQuery(ctx, db.client, queryFactory) + query, _, values, rowScanner := db.buildQuery(ctx, db.client, queryFactory) if query == "" { return 0, errors.ThrowInvalidArgument(nil, "SQL-rWeBw", "invalid query factory") } @@ -68,7 +68,7 @@ func (db *SQL) LatestSequence(ctx context.Context, queryFactory *es_models.Searc } func (db *SQL) InstanceIDs(ctx context.Context, queryFactory *es_models.SearchQueryFactory) ([]string, error) { - query, _, values, rowScanner := buildQuery(ctx, db.client, queryFactory) + query, _, values, rowScanner := db.buildQuery(ctx, db.client, queryFactory) if query == "" { return nil, errors.ThrowInvalidArgument(nil, "SQL-Sfwg2", "invalid query factory") } diff --git a/internal/eventstore/v1/internal/repository/sql/filter_test.go b/internal/eventstore/v1/internal/repository/sql/filter_test.go index df352a236a..75863fed8a 100644 --- a/internal/eventstore/v1/internal/repository/sql/filter_test.go +++ b/internal/eventstore/v1/internal/repository/sql/filter_test.go @@ -123,7 +123,8 @@ func TestSQL_Filter(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sql := &SQL{ - client: &database.DB{DB: tt.fields.client.sqlClient, Database: new(testDB)}, + client: &database.DB{DB: tt.fields.client.sqlClient, Database: new(testDB)}, + allowOrderByCreationDate: true, } events, err := sql.Filter(context.Background(), tt.args.searchQuery) if (err != nil) != tt.res.wantErr { diff --git a/internal/eventstore/v1/internal/repository/sql/query.go b/internal/eventstore/v1/internal/repository/sql/query.go index ed4510e7f7..8b2ea1dc98 100644 --- a/internal/eventstore/v1/internal/repository/sql/query.go +++ b/internal/eventstore/v1/internal/repository/sql/query.go @@ -33,7 +33,7 @@ const ( " FROM eventstore.events" ) -func buildQuery(ctx context.Context, db dialect.Database, queryFactory *es_models.SearchQueryFactory) (query string, limit uint64, values []interface{}, rowScanner func(s scan, dest interface{}) error) { +func (sql *SQL) buildQuery(ctx context.Context, db dialect.Database, queryFactory *es_models.SearchQueryFactory) (query string, limit uint64, values []interface{}, rowScanner func(s scan, dest interface{}) error) { searchQuery, err := queryFactory.Build() if err != nil { logging.New().WithError(err).Warn("search query factory invalid") @@ -51,9 +51,17 @@ func buildQuery(ctx context.Context, db dialect.Database, queryFactory *es_model query += where if searchQuery.Columns == es_models.Columns_Event { - order := " ORDER BY creation_date, event_sequence" - if searchQuery.Desc { - order = " ORDER BY creation_date DESC, event_sequence DESC" + var order string + if sql.allowOrderByCreationDate { + order = " ORDER BY creation_date, event_sequence" + if searchQuery.Desc { + order = " ORDER BY creation_date DESC, event_sequence DESC" + } + } else { + order = " ORDER BY event_sequence" + if searchQuery.Desc { + order = " ORDER BY event_sequence DESC" + } } query += order } diff --git a/internal/eventstore/v1/internal/repository/sql/query_test.go b/internal/eventstore/v1/internal/repository/sql/query_test.go index f0b13bc701..0c4b4d9db9 100644 --- a/internal/eventstore/v1/internal/repository/sql/query_test.go +++ b/internal/eventstore/v1/internal/repository/sql/query_test.go @@ -470,7 +470,7 @@ func Test_buildQuery(t *testing.T) { ctx := context.Background() db := new(testDB) t.Run(tt.name, func(t *testing.T) { - gotQuery, gotLimit, gotValues, gotRowScanner := buildQuery(ctx, db, tt.args.queryFactory) + gotQuery, gotLimit, gotValues, gotRowScanner := (&SQL{allowOrderByCreationDate: true}).buildQuery(ctx, db, tt.args.queryFactory) if gotQuery != tt.res.query { t.Errorf("buildQuery() gotQuery = %v, want %v", gotQuery, tt.res.query) } diff --git a/internal/eventstore/v1/internal/repository/sql/sql.go b/internal/eventstore/v1/internal/repository/sql/sql.go index 3f5ecc431e..ab4c9ca26c 100644 --- a/internal/eventstore/v1/internal/repository/sql/sql.go +++ b/internal/eventstore/v1/internal/repository/sql/sql.go @@ -7,7 +7,8 @@ import ( ) type SQL struct { - client *database.DB + client *database.DB + allowOrderByCreationDate bool } func (db *SQL) Health(ctx context.Context) error { diff --git a/internal/eventstore/v1/query/handler.go b/internal/eventstore/v1/query/handler.go index 7e126a8f2a..384d5206f6 100755 --- a/internal/eventstore/v1/query/handler.go +++ b/internal/eventstore/v1/query/handler.go @@ -17,7 +17,7 @@ const ( type Handler interface { ViewModel() string - EventQuery(instanceIDs []string) (*models.SearchQuery, error) + EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) Reduce(*models.Event) error OnError(event *models.Event, err error) error OnSuccess(instanceIDs []string) error @@ -26,7 +26,7 @@ type Handler interface { QueryLimit() uint64 AggregateTypes() []models.AggregateType - CurrentSequence(instanceID string) (uint64, error) + CurrentSequence(ctx context.Context, instanceID string) (uint64, error) Eventstore() v1.Eventstore Subscription() *v1.Subscription @@ -46,7 +46,7 @@ func ReduceEvent(ctx context.Context, handler Handler, event *models.Event) { ).Error("reduce panicked") } }() - currentSequence, err := handler.CurrentSequence(event.InstanceID) + currentSequence, err := handler.CurrentSequence(ctx, event.InstanceID) if err != nil { logging.WithError(err).Warn("unable to get current sequence") return @@ -67,7 +67,7 @@ func ReduceEvent(ctx context.Context, handler Handler, event *models.Event) { } for _, unprocessedEvent := range unprocessedEvents { - currentSequence, err := handler.CurrentSequence(unprocessedEvent.InstanceID) + currentSequence, err := handler.CurrentSequence(ctx, unprocessedEvent.InstanceID) if err != nil { logging.WithError(err).Warn("unable to get current sequence") return diff --git a/internal/eventstore/v1/spooler/spooler.go b/internal/eventstore/v1/spooler/spooler.go index 24e72ce5fd..6291e448d9 100644 --- a/internal/eventstore/v1/spooler/spooler.go +++ b/internal/eventstore/v1/spooler/spooler.go @@ -126,7 +126,7 @@ func (s *spooledHandler) load(workerID string) { var err error s.succeededOnce, err = s.hasSucceededOnce(ctx) if err != nil { - logging.WithFields("view", s.ViewModel()).OnError(err).Warn("initial lock failed for first schedule") + logging.WithFields("view", s.ViewModel()).OnError(err).Debug("initial lock failed for first schedule") errs <- err return } @@ -222,7 +222,7 @@ func (s *spooledHandler) process(ctx context.Context, events []*models.Event, wo } func (s *spooledHandler) query(ctx context.Context, instanceIDs []string) ([]*models.Event, error) { - query, err := s.EventQuery(instanceIDs) + query, err := s.EventQuery(ctx, instanceIDs) if err != nil { return nil, err } diff --git a/internal/eventstore/v1/spooler/spooler_test.go b/internal/eventstore/v1/spooler/spooler_test.go index 9aa0c75431..862d2aed35 100644 --- a/internal/eventstore/v1/spooler/spooler_test.go +++ b/internal/eventstore/v1/spooler/spooler_test.go @@ -35,7 +35,7 @@ func (h *testHandler) AggregateTypes() []models.AggregateType { return nil } -func (h *testHandler) CurrentSequence(instanceID string) (uint64, error) { +func (h *testHandler) CurrentSequence(ctx context.Context, instanceID string) (uint64, error) { return 0, nil } @@ -51,7 +51,7 @@ func (h *testHandler) Subscription() *v1.Subscription { return nil } -func (h *testHandler) EventQuery(instanceIDs []string) (*models.SearchQuery, error) { +func (h *testHandler) EventQuery(ctx context.Context, instanceIDs []string) (*models.SearchQuery, error) { if h.queryError != nil { return nil, h.queryError } diff --git a/internal/migration/command.go b/internal/migration/command.go index 9d04e3cbe7..6552ef6ed4 100644 --- a/internal/migration/command.go +++ b/internal/migration/command.go @@ -41,14 +41,14 @@ func setupStartedCmd(migration Migration) eventstore.Command { BaseEvent: *eventstore.NewBaseEventForPush( ctx, eventstore.NewAggregate(ctx, aggregateID, aggregateType, "v1"), - startedType), + StartedType), migration: migration, Name: migration.String(), } } -func setupDoneCmd(migration Migration, err error) eventstore.Command { - ctx := authz.SetCtxData(service.WithService(context.Background(), "system"), authz.CtxData{UserID: "system", OrgID: "SYSTEM", ResourceOwner: "SYSTEM"}) +func setupDoneCmd(ctx context.Context, migration Migration, err error) eventstore.Command { + ctx = authz.SetCtxData(service.WithService(ctx, "system"), authz.CtxData{UserID: "system", OrgID: "SYSTEM", ResourceOwner: "SYSTEM"}) typ := doneType var lastRun interface{} if repeatable, ok := migration.(RepeatableMigration); ok { @@ -80,7 +80,7 @@ func (s *SetupStep) Data() interface{} { func (s *SetupStep) UniqueConstraints() []*eventstore.EventUniqueConstraint { switch s.Type() { - case startedType: + case StartedType: return []*eventstore.EventUniqueConstraint{ eventstore.NewAddGlobalEventUniqueConstraint("migration_started", s.migration.String(), "Errors.Step.Started.AlreadyExists"), } @@ -97,7 +97,7 @@ func (s *SetupStep) UniqueConstraints() []*eventstore.EventUniqueConstraint { } func RegisterMappers(es *eventstore.Eventstore) { - es.RegisterFilterEventMapper(aggregateType, startedType, SetupMapper) + es.RegisterFilterEventMapper(aggregateType, StartedType, SetupMapper) es.RegisterFilterEventMapper(aggregateType, doneType, SetupMapper) es.RegisterFilterEventMapper(aggregateType, failedType, SetupMapper) es.RegisterFilterEventMapper(aggregateType, repeatableDoneType, SetupMapper) diff --git a/internal/migration/migration.go b/internal/migration/migration.go index 3608332a8a..63a6cb7b7a 100644 --- a/internal/migration/migration.go +++ b/internal/migration/migration.go @@ -12,7 +12,7 @@ import ( ) const ( - startedType = eventstore.EventType("system.migration.started") + StartedType = eventstore.EventType("system.migration.started") doneType = eventstore.EventType("system.migration.done") failedType = eventstore.EventType("system.migration.failed") repeatableDoneType = eventstore.EventType("system.migration.repeatable.done") @@ -36,7 +36,7 @@ type RepeatableMigration interface { } func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration) (err error) { - logging.Infof("verify migration %s", migration.String()) + logging.WithFields("name", migration.String()).Info("verify migration") if should, err := checkExec(ctx, es, migration); !should || err != nil { return err @@ -46,11 +46,11 @@ func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration return err } - logging.Infof("starting migration %s", migration.String()) + logging.WithFields("name", migration.String()).Info("starting migration") err = migration.Execute(ctx) logging.OnError(err).Error("migration failed") - _, pushErr := es.Push(ctx, setupDoneCmd(migration, err)) + _, pushErr := es.Push(ctx, setupDoneCmd(ctx, migration, err)) logging.OnError(pushErr).Error("migration failed") if err != nil { return err @@ -58,6 +58,48 @@ func Migrate(ctx context.Context, es *eventstore.Eventstore, migration Migration return pushErr } +func LatestStep(ctx context.Context, es *eventstore.Eventstore) (*SetupStep, error) { + events, err := es.Filter(ctx, eventstore.NewSearchQueryBuilder(eventstore.ColumnsEvent). + OrderDesc(). + Limit(1). + AddQuery(). + AggregateTypes(aggregateType). + AggregateIDs(aggregateID). + EventTypes(StartedType, doneType, repeatableDoneType, failedType). + Builder()) + if err != nil { + return nil, err + } + step, ok := events[0].(*SetupStep) + if !ok { + return nil, errors.ThrowInternal(nil, "MIGRA-hppLM", "setup step is malformed") + } + return step, nil +} + +var _ Migration = (*cancelMigration)(nil) + +type cancelMigration struct { + name string +} + +// Execute implements Migration +func (*cancelMigration) Execute(context.Context) error { + return nil +} + +// String implements Migration +func (m *cancelMigration) String() string { + return m.name +} + +var errCancelStep = errors.ThrowError(nil, "MIGRA-zo86K", "migration canceled manually") + +func CancelStep(ctx context.Context, es *eventstore.Eventstore, step *SetupStep) error { + _, err := es.Push(ctx, setupDoneCmd(ctx, &cancelMigration{name: step.Name}, errCancelStep)) + return err +} + // checkExec ensures that only one setup step is done concurrently // if a setup step is already started, it calls shouldExec after some time again func checkExec(ctx context.Context, es *eventstore.Eventstore, migration Migration) (bool, error) { @@ -88,7 +130,7 @@ func shouldExec(ctx context.Context, es *eventstore.Eventstore, migration Migrat AddQuery(). AggregateTypes(aggregateType). AggregateIDs(aggregateID). - EventTypes(startedType, doneType, repeatableDoneType, failedType). + EventTypes(StartedType, doneType, repeatableDoneType, failedType). Builder()) if err != nil { return false, err @@ -106,7 +148,7 @@ func shouldExec(ctx context.Context, es *eventstore.Eventstore, migration Migrat } switch event.Type() { - case startedType, failedType: + case StartedType, failedType: isStarted = !isStarted case doneType, repeatableDoneType: diff --git a/internal/notification/handlers/usernotifier.go b/internal/notification/handlers/usernotifier.go index 542e9287bc..10b0952cee 100644 --- a/internal/notification/handlers/usernotifier.go +++ b/internal/notification/handlers/usernotifier.go @@ -182,6 +182,9 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St if !ok { return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-SWf3g", "reduce.wrong.event.type %s", user.HumanEmailCodeAddedType) } + if e.CodeReturned { + return crdb.NewNoOpStatement(e), nil + } ctx := HandlerContext(event.Aggregate()) alreadyHandled, err := u.checkIfCodeAlreadyHandledOrExpired(ctx, event, e.Expiry, nil, user.UserV1EmailCodeAddedType, user.UserV1EmailCodeSentType, @@ -232,7 +235,7 @@ func (u *userNotifier) reduceEmailCodeAdded(event eventstore.Event) (*handler.St e, u.metricSuccessfulDeliveriesEmail, u.metricFailedDeliveriesEmail, - ).SendEmailVerificationCode(notifyUser, origin, code) + ).SendEmailVerificationCode(notifyUser, origin, code, e.URLTemplate) if err != nil { return nil, err } diff --git a/internal/notification/types/email_verification_code.go b/internal/notification/types/email_verification_code.go index 5f4ab071fb..112da75ddf 100644 --- a/internal/notification/types/email_verification_code.go +++ b/internal/notification/types/email_verification_code.go @@ -1,13 +1,25 @@ package types import ( + "strings" + "github.com/zitadel/zitadel/internal/api/ui/login" "github.com/zitadel/zitadel/internal/domain" "github.com/zitadel/zitadel/internal/query" ) -func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string) error { - url := login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner) +func (notify Notify) SendEmailVerificationCode(user *query.NotifyUser, origin, code string, urlTmpl string) error { + var url string + if urlTmpl == "" { + url = login.MailVerificationLink(origin, user.ID, code, user.ResourceOwner) + } else { + var buf strings.Builder + if err := domain.RenderConfirmURLTemplate(&buf, urlTmpl, user.ID, code, user.ResourceOwner); err != nil { + return err + } + url = buf.String() + } + args := make(map[string]interface{}) args["Code"] = code return notify(url, args, domain.VerifyEmailMessageType, true) diff --git a/internal/notification/types/email_verification_code_test.go b/internal/notification/types/email_verification_code_test.go new file mode 100644 index 0000000000..0e1975fbc3 --- /dev/null +++ b/internal/notification/types/email_verification_code_test.go @@ -0,0 +1,107 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query" +) + +func TestNotify_SendEmailVerificationCode(t *testing.T) { + type res struct { + url string + args map[string]interface{} + messageType string + allowUnverifiedNotificationChannel bool + } + notify := func(dst *res) Notify { + return func( + url string, + args map[string]interface{}, + messageType string, + allowUnverifiedNotificationChannel bool, + ) error { + dst.url = url + dst.args = args + dst.messageType = messageType + dst.allowUnverifiedNotificationChannel = allowUnverifiedNotificationChannel + return nil + } + } + + type args struct { + user *query.NotifyUser + origin string + code string + urlTmpl string + } + tests := []struct { + name string + args args + want *res + wantErr error + }{ + { + name: "default URL", + args: args{ + user: &query.NotifyUser{ + ID: "user1", + ResourceOwner: "org1", + }, + origin: "https://example.com", + code: "123", + urlTmpl: "", + }, + want: &res{ + url: "https://example.com/ui/login/mail/verification?userID=user1&code=123&orgID=org1", + args: map[string]interface{}{"Code": "123"}, + messageType: domain.VerifyEmailMessageType, + allowUnverifiedNotificationChannel: true, + }, + }, + { + name: "template error", + args: args{ + user: &query.NotifyUser{ + ID: "user1", + ResourceOwner: "org1", + }, + origin: "https://example.com", + code: "123", + urlTmpl: "{{", + }, + want: &res{}, + wantErr: caos_errs.ThrowInvalidArgument(nil, "USERv2-ooD8p", "Errors.User.Email.InvalidURLTemplate"), + }, + { + name: "template success", + args: args{ + user: &query.NotifyUser{ + ID: "user1", + ResourceOwner: "org1", + }, + origin: "https://example.com", + code: "123", + urlTmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}", + }, + want: &res{ + url: "https://example.com/email/verify?userID=user1&code=123&orgID=org1", + args: map[string]interface{}{"Code": "123"}, + messageType: domain.VerifyEmailMessageType, + allowUnverifiedNotificationChannel: true, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(res) + err := notify(got).SendEmailVerificationCode(tt.args.user, tt.args.origin, tt.args.code, tt.args.urlTmpl) + require.ErrorIs(t, err, tt.wantErr) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/org/repository/view/query.go b/internal/org/repository/view/query.go index c6352bff4a..765d6abbc8 100644 --- a/internal/org/repository/view/query.go +++ b/internal/org/repository/view/query.go @@ -16,5 +16,19 @@ func OrgByIDQuery(id, instanceID string, latestSequence uint64) (*es_models.Sear LatestSequenceFilter(latestSequence). InstanceIDFilter(instanceID). AggregateIDFilter(id). + EventTypesFilter( + es_models.EventType(org.OrgAddedEventType), + es_models.EventType(org.OrgChangedEventType), + es_models.EventType(org.OrgDeactivatedEventType), + es_models.EventType(org.OrgReactivatedEventType), + es_models.EventType(org.OrgDomainAddedEventType), + es_models.EventType(org.OrgDomainVerificationAddedEventType), + es_models.EventType(org.OrgDomainVerifiedEventType), + es_models.EventType(org.OrgDomainPrimarySetEventType), + es_models.EventType(org.OrgDomainRemovedEventType), + es_models.EventType(org.DomainPolicyAddedEventType), + es_models.EventType(org.DomainPolicyChangedEventType), + es_models.EventType(org.DomainPolicyRemovedEventType), + ). SearchQuery(), nil } diff --git a/internal/project/repository/eventsourcing/model/project.go b/internal/project/repository/eventsourcing/model/project.go index 8aba9a08ab..dfba580f7f 100644 --- a/internal/project/repository/eventsourcing/model/project.go +++ b/internal/project/repository/eventsourcing/model/project.go @@ -18,15 +18,26 @@ type Project struct { ProjectRoleCheck bool `json:"projectRoleCheck,omitempty"` HasProjectCheck bool `json:"hasProjectCheck,omitempty"` State int32 `json:"-"` + OIDCApplications []*oidcApp +} + +type oidcApp struct { + AppID string `json:"appId"` + ClientID string `json:"clientId,omitempty"` } func ProjectToModel(project *Project) *model.Project { + apps := make([]*model.Application, len(project.OIDCApplications)) + for i, application := range project.OIDCApplications { + apps[i] = &model.Application{OIDCConfig: &model.OIDCConfig{ClientID: application.ClientID}} + } return &model.Project{ ObjectRoot: project.ObjectRoot, Name: project.Name, ProjectRoleAssertion: project.ProjectRoleAssertion, ProjectRoleCheck: project.ProjectRoleCheck, State: model.ProjectState(project.State), + Applications: apps, } } @@ -59,6 +70,10 @@ func (p *Project) AppendEvent(event *es_models.Event) error { return p.appendReactivatedEvent() case project.ProjectRemovedType: return p.appendRemovedEvent() + case project.OIDCConfigAddedType: + return p.appendOIDCConfig(event) + case project.ApplicationRemovedType: + return p.appendApplicationRemoved(event) } return nil } @@ -84,6 +99,31 @@ func (p *Project) appendRemovedEvent() error { return nil } +func (p *Project) appendOIDCConfig(event *es_models.Event) error { + appEvent := new(oidcApp) + if err := json.Unmarshal(event.Data, appEvent); err != nil { + return err + } + p.OIDCApplications = append(p.OIDCApplications, appEvent) + return nil +} + +func (p *Project) appendApplicationRemoved(event *es_models.Event) error { + appEvent := new(oidcApp) + if err := json.Unmarshal(event.Data, appEvent); err != nil { + return err + } + for i := len(p.OIDCApplications) - 1; i >= 0; i-- { + if p.OIDCApplications[i].AppID == appEvent.AppID { + p.OIDCApplications[i] = p.OIDCApplications[len(p.OIDCApplications)-1] + p.OIDCApplications[len(p.OIDCApplications)-1] = nil + p.OIDCApplications = p.OIDCApplications[:len(p.OIDCApplications)-1] + return nil + } + } + return nil +} + func (p *Project) SetData(event *es_models.Event) error { if err := json.Unmarshal(event.Data, p); err != nil { logging.Log("EVEN-lo9sr").WithError(err).Error("could not unmarshal event data") diff --git a/internal/project/repository/view/query.go b/internal/project/repository/view/query.go index 963e1cd4a9..56c8ae007b 100644 --- a/internal/project/repository/view/query.go +++ b/internal/project/repository/view/query.go @@ -16,5 +16,14 @@ func ProjectByIDQuery(id, instanceID string, latestSequence uint64) (*es_models. AggregateTypeFilter(project.AggregateType). LatestSequenceFilter(latestSequence). InstanceIDFilter(instanceID). + EventTypesFilter( + es_models.EventType(project.ProjectAddedType), + es_models.EventType(project.ProjectChangedType), + es_models.EventType(project.ProjectDeactivatedType), + es_models.EventType(project.ProjectReactivatedType), + es_models.EventType(project.ProjectRemovedType), + es_models.EventType(project.OIDCConfigAddedType), + es_models.EventType(project.ApplicationRemovedType), + ). SearchQuery(), nil } diff --git a/internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl b/internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl new file mode 100644 index 0000000000..fc47ff49e8 --- /dev/null +++ b/internal/protoc/protoc-gen-auth/auth_method_mapping.go.tmpl @@ -0,0 +1,16 @@ +// Code generated by protoc-gen-auth. DO NOT EDIT. + +package {{.GoPackageName}} + +import ( + "github.com/zitadel/zitadel/internal/api/authz" +) + +var {{.ServiceName}}_AuthMethods = authz.MethodMapping { + {{ range $m := .AuthOptions}} + {{$.ServiceName}}_{{$m.Name}}_FullMethodName: authz.Option{ + Permission: "{{$m.Permission}}", + CheckParam: "{{$m.CheckFieldName}}", + }, + {{ end}} +} diff --git a/internal/protoc/protoc-gen-auth/main.go b/internal/protoc/protoc-gen-auth/main.go new file mode 100644 index 0000000000..b0e78c6990 --- /dev/null +++ b/internal/protoc/protoc-gen-auth/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "bytes" + _ "embed" + "fmt" + "io" + "os" + "text/template" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" + + "github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption" +) + +var ( + //go:embed auth_method_mapping.go.tmpl + authTemplate []byte +) + +type authMethods struct { + GoPackageName string + ProtoPackageName string + ServiceName string + AuthOptions []authOption +} + +type authOption struct { + Name string + Permission string + CheckFieldName string +} + +func main() { + + input, _ := io.ReadAll(os.Stdin) + var req pluginpb.CodeGeneratorRequest + err := proto.Unmarshal(input, &req) + if err != nil { + panic(err) + } + + opts := protogen.Options{} + plugin, err := opts.New(&req) + if err != nil { + panic(err) + } + plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + + authTemp := loadTemplate(authTemplate) + + for _, file := range plugin.Files { + + var buf bytes.Buffer + + var methods authMethods + for _, service := range file.Services { + methods.ServiceName = service.GoName + methods.GoPackageName = string(file.GoPackageName) + methods.ProtoPackageName = *file.Proto.Package + for _, method := range service.Methods { + if options := method.Desc.Options().(*descriptorpb.MethodOptions); options != nil { + ext := proto.GetExtension(options, authoption.E_AuthOption).(*authoption.AuthOption) + if ext != nil { + methods.AuthOptions = append(methods.AuthOptions, authOption{Name: string(method.Desc.Name()), Permission: ext.Permission, CheckFieldName: ext.CheckFieldName}) + } + } + } + } + if len(methods.AuthOptions) > 0 { + authTemp.Execute(&buf, &methods) + + filename := file.GeneratedFilenamePrefix + ".pb.authoptions.go" + file := plugin.NewGeneratedFile(filename, ".") + + file.Write(buf.Bytes()) + } + } + + // Generate a response from our plugin and marshall as protobuf + stdout := plugin.Response() + out, err := proto.Marshal(stdout) + if err != nil { + panic(err) + } + + // Write the response to stdout, to be picked up by protoc + fmt.Fprintf(os.Stdout, string(out)) +} + +func loadTemplate(templateData []byte) *template.Template { + return template.Must(template.New(""). + Parse(string(templateData))) +} diff --git a/internal/protoc/protoc-gen-authoption/README.md b/internal/protoc/protoc-gen-authoption/README.md deleted file mode 100644 index 793c575692..0000000000 --- a/internal/protoc/protoc-gen-authoption/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# protoc-gen-authoption - -Proto options to annotate auth methods in protos - -## Generate protos/templates -protos: `go generate authoption/generate.go` -templates/install: `go generate generate.go` - -## Usage -``` -// proto file -import "authoption/options.proto"; - -service MyService { - - rpc Hello(Hello) returns (google.protobuf.Empty) { - option (google.api.http) = { - get: "/hello" - }; - - option (caos.zitadel.utils.v1.auth_option) = { - zitadel_permission: "hello.read" - zitadel_check_param: "id" - }; - } - - message Hello { - string id = 1; - } -} -``` -Caos Auth Option is used for granting groups -On each zitadel role is specified which auth methods are allowed to call - -Get protoc-get-authoption: ``go get github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption`` - -Protc-Flag: ``--authoption_out=.`` \ No newline at end of file diff --git a/internal/protoc/protoc-gen-authoption/generate.go b/internal/protoc/protoc-gen-authoption/generate.go deleted file mode 100644 index 3278b63e6f..0000000000 --- a/internal/protoc/protoc-gen-authoption/generate.go +++ /dev/null @@ -1,4 +0,0 @@ -package main - -//go:generate go-bindata -pkg main -o templates.gen.go templates -//go:generate go install diff --git a/internal/protoc/protoc-gen-authoption/main.go b/internal/protoc/protoc-gen-authoption/main.go deleted file mode 100644 index 5e4cbd683e..0000000000 --- a/internal/protoc/protoc-gen-authoption/main.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - base "github.com/zitadel/zitadel/internal/protoc/protoc-base" - "github.com/zitadel/zitadel/internal/protoc/protoc-gen-authoption/authoption" -) - -const ( - fileName = "%v.pb.authoptions.go" -) - -func main() { - base.RegisterExtension(authoption.E_AuthOption) - base.RunWithBaseTemplate(fileName, base.LoadTemplate(templatesAuth_method_mappingGoTmplBytes())) -} diff --git a/internal/protoc/protoc-gen-authoption/templates/auth_method_mapping.go.tmpl b/internal/protoc/protoc-gen-authoption/templates/auth_method_mapping.go.tmpl deleted file mode 100644 index bd6fe3b881..0000000000 --- a/internal/protoc/protoc-gen-authoption/templates/auth_method_mapping.go.tmpl +++ /dev/null @@ -1,33 +0,0 @@ -// Code generated by protoc-gen-authmethod. DO NOT EDIT. - -package {{.File.GoPkg.Name}} - - -import ( - "google.golang.org/grpc" - - "github.com/zitadel/zitadel/internal/api/authz" - "github.com/zitadel/zitadel/internal/api/grpc/server/middleware" -) - -{{ range $s := .File.Services }} - -/** - * {{$s.Name}} - */ - -const {{$s.Name}}_MethodPrefix = "{{$.File.Package}}.{{$s.Name}}" - -var {{$s.Name}}_AuthMethods = authz.MethodMapping { - {{ range $m := $s.Method}} - {{ $mAuthOpt := option $m.Options "zitadel.v1.auth_option" }} - {{ if and $mAuthOpt $mAuthOpt.Permission }} - "/{{$.File.Package}}.{{$s.Name}}/{{.Name}}": authz.Option{ - Permission: "{{$mAuthOpt.Permission}}", - CheckParam: "{{$mAuthOpt.CheckFieldName}}", - }, - {{end}} - {{ end}} -} - -{{ end }} diff --git a/internal/query/device_auth.go b/internal/query/device_auth.go new file mode 100644 index 0000000000..98faff200b --- /dev/null +++ b/internal/query/device_auth.go @@ -0,0 +1,141 @@ +package query + +import ( + "context" + "database/sql" + errs "errors" + + sq "github.com/Masterminds/squirrel" + + "github.com/zitadel/zitadel/internal/api/authz" + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/query/projection" + "github.com/zitadel/zitadel/internal/telemetry/tracing" +) + +var ( + deviceAuthTable = table{ + name: projection.DeviceAuthProjectionTable, + instanceIDCol: projection.DeviceAuthColumnInstanceID, + } + DeviceAuthColumnID = Column{ + name: projection.DeviceAuthColumnID, + table: deviceAuthTable, + } + DeviceAuthColumnClientID = Column{ + name: projection.DeviceAuthColumnClientID, + table: deviceAuthTable, + } + DeviceAuthColumnDeviceCode = Column{ + name: projection.DeviceAuthColumnDeviceCode, + table: deviceAuthTable, + } + DeviceAuthColumnUserCode = Column{ + name: projection.DeviceAuthColumnUserCode, + table: deviceAuthTable, + } + DeviceAuthColumnExpires = Column{ + name: projection.DeviceAuthColumnExpires, + table: deviceAuthTable, + } + DeviceAuthColumnScopes = Column{ + name: projection.DeviceAuthColumnScopes, + table: deviceAuthTable, + } + DeviceAuthColumnState = Column{ + name: projection.DeviceAuthColumnState, + table: deviceAuthTable, + } + DeviceAuthColumnSubject = Column{ + name: projection.DeviceAuthColumnSubject, + table: deviceAuthTable, + } + DeviceAuthColumnCreationDate = Column{ + name: projection.DeviceAuthColumnCreationDate, + table: deviceAuthTable, + } + DeviceAuthColumnChangeDate = Column{ + name: projection.DeviceAuthColumnChangeDate, + table: deviceAuthTable, + } + DeviceAuthColumnSequence = Column{ + name: projection.DeviceAuthColumnSequence, + table: deviceAuthTable, + } + DeviceAuthColumnInstanceID = Column{ + name: projection.DeviceAuthColumnInstanceID, + table: deviceAuthTable, + } +) + +func (q *Queries) DeviceAuthByDeviceCode(ctx context.Context, clientID, deviceCode string) (_ *domain.DeviceAuth, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, scan := prepareDeviceAuthQuery(ctx, q.client) + eq := sq.Eq{ + DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + DeviceAuthColumnClientID.identifier(): clientID, + DeviceAuthColumnDeviceCode.identifier(): deviceCode, + } + query, args, err := stmt.Where(eq).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-uk1Oh", "Errors.Query.SQLStatement") + } + + return scan(q.client.QueryRowContext(ctx, query, args...)) +} + +func (q *Queries) DeviceAuthByUserCode(ctx context.Context, userCode string) (_ *domain.DeviceAuth, err error) { + ctx, span := tracing.NewSpan(ctx) + defer func() { span.EndWithError(err) }() + + stmt, scan := prepareDeviceAuthQuery(ctx, q.client) + eq := sq.Eq{ + DeviceAuthColumnInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(), + DeviceAuthColumnUserCode.identifier(): userCode, + } + query, args, err := stmt.Where(eq).ToSql() + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Axu7l", "Errors.Query.SQLStatement") + } + + return scan(q.client.QueryRowContext(ctx, query, args...)) +} + +var deviceAuthSelectColumns = []string{ + DeviceAuthColumnID.identifier(), + DeviceAuthColumnClientID.identifier(), + DeviceAuthColumnScopes.identifier(), + DeviceAuthColumnExpires.identifier(), + DeviceAuthColumnState.identifier(), + DeviceAuthColumnSubject.identifier(), +} + +func prepareDeviceAuthQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*domain.DeviceAuth, error)) { + return sq.Select(deviceAuthSelectColumns...).From(deviceAuthTable.identifier()).PlaceholderFormat(sq.Dollar), + func(row *sql.Row) (*domain.DeviceAuth, error) { + dst := new(domain.DeviceAuth) + var scopes database.StringArray + + err := row.Scan( + &dst.AggregateID, + &dst.ClientID, + &scopes, + &dst.Expires, + &dst.State, + &dst.Subject, + ) + if errs.Is(err, sql.ErrNoRows) { + return nil, errors.ThrowNotFound(err, "QUERY-Sah9a", "Errors.DeviceAuth.NotExisting") + } + if err != nil { + return nil, errors.ThrowInternal(err, "QUERY-Voo3o", "Errors.Internal") + } + + dst.Scopes = scopes + return dst, nil + } +} diff --git a/internal/query/device_auth_test.go b/internal/query/device_auth_test.go new file mode 100644 index 0000000000..938cb9f844 --- /dev/null +++ b/internal/query/device_auth_test.go @@ -0,0 +1,158 @@ +package query + +import ( + "context" + "database/sql" + "database/sql/driver" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/zitadel/zitadel/internal/database" + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" +) + +const ( + expectedDeviceAuthQueryC = `SELECT` + + ` projections.device_authorizations.id,` + + ` projections.device_authorizations.client_id,` + + ` projections.device_authorizations.scopes,` + + ` projections.device_authorizations.expires,` + + ` projections.device_authorizations.state,` + + ` projections.device_authorizations.subject` + + ` FROM projections.device_authorizations` + expectedDeviceAuthWhereDeviceCodeQueryC = expectedDeviceAuthQueryC + + ` WHERE projections.device_authorizations.client_id = $1` + + ` AND projections.device_authorizations.device_code = $2` + + ` AND projections.device_authorizations.instance_id = $3` + expectedDeviceAuthWhereUserCodeQueryC = expectedDeviceAuthQueryC + + ` WHERE projections.device_authorizations.instance_id = $1` + + ` AND projections.device_authorizations.user_code = $2` +) + +var ( + expectedDeviceAuthQuery = regexp.QuoteMeta(expectedDeviceAuthQueryC) + expectedDeviceAuthWhereDeviceCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereDeviceCodeQueryC) + expectedDeviceAuthWhereUserCodeQuery = regexp.QuoteMeta(expectedDeviceAuthWhereUserCodeQueryC) + expectedDeviceAuthValues = []driver.Value{ + "primary-id", + "client-id", + database.StringArray{"a", "b", "c"}, + testNow, + domain.DeviceAuthStateApproved, + "subject", + } + expectedDeviceAuth = &domain.DeviceAuth{ + ObjectRoot: models.ObjectRoot{ + AggregateID: "primary-id", + }, + ClientID: "client-id", + Scopes: []string{"a", "b", "c"}, + Expires: testNow, + State: domain.DeviceAuthStateApproved, + Subject: "subject", + } +) + +func TestQueries_DeviceAuthByDeviceCode(t *testing.T) { + client, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to build mock client: %v", err) + } + defer client.Close() + + mock.ExpectQuery(expectedDeviceAuthWhereDeviceCodeQuery).WillReturnRows( + sqlmock.NewRows(deviceAuthSelectColumns).AddRow(expectedDeviceAuthValues...), + ) + q := Queries{ + client: &database.DB{DB: client}, + } + got, err := q.DeviceAuthByDeviceCode(context.TODO(), "123", "456") + require.NoError(t, err) + assert.Equal(t, expectedDeviceAuth, got) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func TestQueries_DeviceAuthByUserCode(t *testing.T) { + client, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("failed to build mock client: %v", err) + } + defer client.Close() + + mock.ExpectQuery(expectedDeviceAuthWhereUserCodeQuery).WillReturnRows( + sqlmock.NewRows(deviceAuthSelectColumns).AddRow(expectedDeviceAuthValues...), + ) + q := Queries{ + client: &database.DB{DB: client}, + } + got, err := q.DeviceAuthByUserCode(context.TODO(), "789") + require.NoError(t, err) + assert.Equal(t, expectedDeviceAuth, got) + require.NoError(t, mock.ExpectationsWereMet()) +} + +func Test_prepareDeviceAuthQuery(t *testing.T) { + type want struct { + sqlExpectations sqlExpectation + err checkErr + } + tests := []struct { + name string + want want + object any + }{ + { + name: "success", + want: want{ + sqlExpectations: mockQueries( + expectedDeviceAuthQuery, + deviceAuthSelectColumns, + [][]driver.Value{expectedDeviceAuthValues}, + ), + }, + object: expectedDeviceAuth, + }, + { + name: "not found error", + want: want{ + sqlExpectations: mockQueryErr( + expectedDeviceAuthQuery, + sql.ErrNoRows, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrNoRows) { + return fmt.Errorf("err should be sql.ErrNoRows got: %w", err), false + } + return nil, true + }, + }, + }, + { + name: "other error", + want: want{ + sqlExpectations: mockQueryErr( + expectedDeviceAuthQuery, + sql.ErrConnDone, + ), + err: func(err error) (error, bool) { + if !errors.Is(err, sql.ErrConnDone) { + return fmt.Errorf("err should be sql.ErrConnDone got: %w", err), false + } + return nil, true + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assertPrepare(t, prepareDeviceAuthQuery, tt.object, tt.want.sqlExpectations, tt.want.err, defaultPrepareArgs...) + }) + } +} diff --git a/internal/query/projection/device_auth.go b/internal/query/projection/device_auth.go new file mode 100644 index 0000000000..c678dbd301 --- /dev/null +++ b/internal/query/projection/device_auth.go @@ -0,0 +1,161 @@ +package projection + +import ( + "context" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/eventstore/handler" + "github.com/zitadel/zitadel/internal/eventstore/handler/crdb" + "github.com/zitadel/zitadel/internal/repository/deviceauth" +) + +const ( + DeviceAuthProjectionTable = "projections.device_authorizations" + + DeviceAuthColumnID = "id" + DeviceAuthColumnClientID = "client_id" + DeviceAuthColumnDeviceCode = "device_code" + DeviceAuthColumnUserCode = "user_code" + DeviceAuthColumnExpires = "expires" + DeviceAuthColumnScopes = "scopes" + DeviceAuthColumnState = "state" + DeviceAuthColumnSubject = "subject" + + DeviceAuthColumnCreationDate = "creation_date" + DeviceAuthColumnChangeDate = "change_date" + DeviceAuthColumnSequence = "sequence" + DeviceAuthColumnInstanceID = "instance_id" +) + +type deviceAuthProjection struct { + crdb.StatementHandler +} + +func newDeviceAuthProjection(ctx context.Context, config crdb.StatementHandlerConfig) *deviceAuthProjection { + p := new(deviceAuthProjection) + config.ProjectionName = DeviceAuthProjectionTable + config.Reducers = p.reducers() + config.InitCheck = crdb.NewTableCheck( + crdb.NewTable([]*crdb.Column{ + crdb.NewColumn(DeviceAuthColumnID, crdb.ColumnTypeText), + crdb.NewColumn(DeviceAuthColumnClientID, crdb.ColumnTypeText), + crdb.NewColumn(DeviceAuthColumnDeviceCode, crdb.ColumnTypeText), + crdb.NewColumn(DeviceAuthColumnUserCode, crdb.ColumnTypeText), + crdb.NewColumn(DeviceAuthColumnExpires, crdb.ColumnTypeTimestamp), + crdb.NewColumn(DeviceAuthColumnScopes, crdb.ColumnTypeTextArray), + crdb.NewColumn(DeviceAuthColumnState, crdb.ColumnTypeEnum, crdb.Default(domain.DeviceAuthStateInitiated)), + crdb.NewColumn(DeviceAuthColumnSubject, crdb.ColumnTypeText, crdb.Default("")), + crdb.NewColumn(DeviceAuthColumnCreationDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(DeviceAuthColumnChangeDate, crdb.ColumnTypeTimestamp), + crdb.NewColumn(DeviceAuthColumnSequence, crdb.ColumnTypeInt64), + crdb.NewColumn(DeviceAuthColumnInstanceID, crdb.ColumnTypeText), + }, + crdb.NewPrimaryKey(DeviceAuthColumnInstanceID, DeviceAuthColumnID), + crdb.WithIndex(crdb.NewIndex("user_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnUserCode})), + crdb.WithIndex(crdb.NewIndex("device_code", []string{DeviceAuthColumnInstanceID, DeviceAuthColumnClientID, DeviceAuthColumnDeviceCode})), + ), + ) + + p.StatementHandler = crdb.NewStatementHandler(ctx, config) + return p +} + +func (p *deviceAuthProjection) reducers() []handler.AggregateReducer { + return []handler.AggregateReducer{ + { + Aggregate: deviceauth.AggregateType, + EventRedusers: []handler.EventReducer{ + { + Event: deviceauth.AddedEventType, + Reduce: p.reduceAdded, + }, + { + Event: deviceauth.ApprovedEventType, + Reduce: p.reduceAppoved, + }, + { + Event: deviceauth.CanceledEventType, + Reduce: p.reduceCanceled, + }, + { + Event: deviceauth.RemovedEventType, + Reduce: p.reduceRemoved, + }, + }, + }, + } +} + +func (p *deviceAuthProjection) reduceAdded(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*deviceauth.AddedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-chu6O", "reduce.wrong.event.type %T != %s", event, deviceauth.AddedEventType) + } + return crdb.NewCreateStatement( + e, + []handler.Column{ + handler.NewCol(DeviceAuthColumnID, e.Aggregate().ID), + handler.NewCol(DeviceAuthColumnClientID, e.ClientID), + handler.NewCol(DeviceAuthColumnDeviceCode, e.DeviceCode), + handler.NewCol(DeviceAuthColumnUserCode, e.UserCode), + handler.NewCol(DeviceAuthColumnExpires, e.Expires), + handler.NewCol(DeviceAuthColumnScopes, e.Scopes), + handler.NewCol(DeviceAuthColumnCreationDate, e.CreationDate()), + handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), + handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), + handler.NewCol(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), + }, + ), nil +} + +func (p *deviceAuthProjection) reduceAppoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*deviceauth.ApprovedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-kei0A", "reduce.wrong.event.type %T != %s", event, deviceauth.ApprovedEventType) + } + return crdb.NewUpdateStatement(e, + []handler.Column{ + handler.NewCol(DeviceAuthColumnState, domain.DeviceAuthStateApproved), + handler.NewCol(DeviceAuthColumnSubject, e.Subject), + handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), + handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), + }, + ), nil +} + +func (p *deviceAuthProjection) reduceCanceled(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*deviceauth.CanceledEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-eeS8d", "reduce.wrong.event.type %T != %s", event, deviceauth.CanceledEventType) + } + return crdb.NewUpdateStatement(e, + []handler.Column{ + handler.NewCol(DeviceAuthColumnState, e.Reason.State()), + handler.NewCol(DeviceAuthColumnChangeDate, e.CreationDate()), + handler.NewCol(DeviceAuthColumnSequence, e.Sequence()), + }, + []handler.Condition{ + handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), + }, + ), nil +} + +func (p *deviceAuthProjection) reduceRemoved(event eventstore.Event) (*handler.Statement, error) { + e, ok := event.(*deviceauth.RemovedEvent) + if !ok { + return nil, errors.ThrowInvalidArgumentf(nil, "HANDL-AJi1u", "reduce.wrong.event.type %T != %s", event, deviceauth.RemovedEventType) + } + return crdb.NewDeleteStatement(e, + []handler.Condition{ + handler.NewCond(DeviceAuthColumnInstanceID, e.Aggregate().InstanceID), + handler.NewCond(DeviceAuthColumnID, e.Aggregate().ID), + }, + ), nil +} diff --git a/internal/query/projection/projection.go b/internal/query/projection/projection.go index e3c5cd4f71..fb461927c2 100644 --- a/internal/query/projection/projection.go +++ b/internal/query/projection/projection.go @@ -64,6 +64,7 @@ var ( NotificationPolicyProjection *notificationPolicyProjection NotificationsProjection interface{} NotificationsQuotaProjection interface{} + DeviceAuthProjection *deviceAuthProjection ) type projection interface { @@ -139,6 +140,7 @@ func Create(ctx context.Context, sqlClient *database.DB, es *eventstore.Eventsto KeyProjection = newKeyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["keys"]), keyEncryptionAlgorithm, certEncryptionAlgorithm) SecurityPolicyProjection = newSecurityPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["security_policies"])) NotificationPolicyProjection = newNotificationPolicyProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["notification_policies"])) + DeviceAuthProjection = newDeviceAuthProjection(ctx, applyCustomConfig(projectionConfig, config.Customizations["device_auth"])) newProjectionsList() return nil } @@ -234,5 +236,6 @@ func newProjectionsList() { KeyProjection, SecurityPolicyProjection, NotificationPolicyProjection, + DeviceAuthProjection, } } diff --git a/internal/repository/deviceauth/aggregate.go b/internal/repository/deviceauth/aggregate.go new file mode 100644 index 0000000000..da3645d112 --- /dev/null +++ b/internal/repository/deviceauth/aggregate.go @@ -0,0 +1,19 @@ +package deviceauth + +import "github.com/zitadel/zitadel/internal/eventstore" + +const ( + AggregateType = "device_auth" + AggregateVersion = "v1" +) + +func NewAggregate(aggrID, instanceID string) *eventstore.Aggregate { + return &eventstore.Aggregate{ + ID: aggrID, + Type: AggregateType, + // we use the id because we don't know the resource owner yet + ResourceOwner: instanceID, + InstanceID: instanceID, + Version: AggregateVersion, + } +} diff --git a/internal/repository/deviceauth/constraints.go b/internal/repository/deviceauth/constraints.go new file mode 100644 index 0000000000..679220524c --- /dev/null +++ b/internal/repository/deviceauth/constraints.go @@ -0,0 +1,46 @@ +package deviceauth + +import ( + "strings" + + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + UniqueUserCode = "user_code" + UniqueDeviceCode = "device_code" + DuplicateUserCode = "Errors.DeviceUserCode.AlreadyExists" + DuplicateDeviceCode = "Errors.DeviceCode.AlreadyExists" +) + +func deviceCodeUniqueField(clientID, deviceCode string) string { + return strings.Join([]string{clientID, deviceCode}, ":") +} + +func NewAddUniqueConstraints(clientID, deviceCode, userCode string) []*eventstore.EventUniqueConstraint { + return []*eventstore.EventUniqueConstraint{ + eventstore.NewAddEventUniqueConstraint( + UniqueDeviceCode, + deviceCodeUniqueField(clientID, deviceCode), + DuplicateDeviceCode, + ), + eventstore.NewAddEventUniqueConstraint( + UniqueUserCode, + userCode, + DuplicateUserCode, + ), + } +} + +func NewRemoveUniqueConstraints(clientID, deviceCode, userCode string) []*eventstore.EventUniqueConstraint { + return []*eventstore.EventUniqueConstraint{ + eventstore.NewRemoveEventUniqueConstraint( + UniqueDeviceCode, + deviceCodeUniqueField(clientID, deviceCode), + ), + eventstore.NewRemoveEventUniqueConstraint( + UniqueUserCode, + userCode, + ), + } +} diff --git a/internal/repository/deviceauth/device_auth.go b/internal/repository/deviceauth/device_auth.go new file mode 100644 index 0000000000..0ece3e78f9 --- /dev/null +++ b/internal/repository/deviceauth/device_auth.go @@ -0,0 +1,141 @@ +package deviceauth + +import ( + "context" + "time" + + "github.com/zitadel/zitadel/internal/domain" + "github.com/zitadel/zitadel/internal/eventstore" +) + +const ( + eventTypePrefix eventstore.EventType = "device.authorization." + AddedEventType = eventTypePrefix + "added" + ApprovedEventType = eventTypePrefix + "approved" + CanceledEventType = eventTypePrefix + "canceled" + RemovedEventType = eventTypePrefix + "removed" +) + +type AddedEvent struct { + *eventstore.BaseEvent + + ClientID string + DeviceCode string + UserCode string + Expires time.Time + Scopes []string + State domain.DeviceAuthState +} + +func (e *AddedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *AddedEvent) Data() any { + return e +} + +func (e *AddedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return NewAddUniqueConstraints(e.ClientID, e.DeviceCode, e.UserCode) +} + +func NewAddedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + clientID string, + deviceCode string, + userCode string, + expires time.Time, + scopes []string, +) *AddedEvent { + return &AddedEvent{ + eventstore.NewBaseEventForPush( + ctx, aggregate, AddedEventType, + ), + clientID, deviceCode, userCode, expires, scopes, domain.DeviceAuthStateInitiated} +} + +type ApprovedEvent struct { + *eventstore.BaseEvent + + Subject string +} + +func (e *ApprovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *ApprovedEvent) Data() any { + return e +} + +func (e *ApprovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewApprovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + subject string, +) *ApprovedEvent { + return &ApprovedEvent{ + eventstore.NewBaseEventForPush( + ctx, aggregate, ApprovedEventType, + ), + subject, + } +} + +type CanceledEvent struct { + *eventstore.BaseEvent + Reason domain.DeviceAuthCanceled +} + +func (e *CanceledEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *CanceledEvent) Data() any { + return e +} + +func (e *CanceledEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return nil +} + +func NewCanceledEvent(ctx context.Context, aggregate *eventstore.Aggregate, reason domain.DeviceAuthCanceled) *CanceledEvent { + return &CanceledEvent{eventstore.NewBaseEventForPush(ctx, aggregate, CanceledEventType), reason} +} + +type RemovedEvent struct { + *eventstore.BaseEvent + + ClientID string + DeviceCode string + UserCode string +} + +func (e *RemovedEvent) SetBaseEvent(b *eventstore.BaseEvent) { + e.BaseEvent = b +} + +func (e *RemovedEvent) Data() any { + return e +} + +func (e *RemovedEvent) UniqueConstraints() []*eventstore.EventUniqueConstraint { + return NewRemoveUniqueConstraints(e.ClientID, e.DeviceCode, e.UserCode) +} + +func NewRemovedEvent( + ctx context.Context, + aggregate *eventstore.Aggregate, + clientID, deviceCode, userCode string, +) *RemovedEvent { + return &RemovedEvent{ + eventstore.NewBaseEventForPush( + ctx, aggregate, RemovedEventType, + ), + clientID, deviceCode, userCode, + } +} diff --git a/internal/repository/org/eventstore.go b/internal/repository/org/eventstore.go index fb85ca86ee..662bf77b4b 100644 --- a/internal/repository/org/eventstore.go +++ b/internal/repository/org/eventstore.go @@ -2,6 +2,7 @@ package org import ( "github.com/zitadel/zitadel/internal/eventstore" + "github.com/zitadel/zitadel/internal/repository/deviceauth" ) func RegisterEventMappers(es *eventstore.Eventstore) { @@ -107,5 +108,9 @@ func RegisterEventMappers(es *eventstore.Eventstore) { RegisterFilterEventMapper(AggregateType, MetadataRemovedAllType, MetadataRemovedAllEventMapper). RegisterFilterEventMapper(AggregateType, NotificationPolicyAddedEventType, NotificationPolicyAddedEventMapper). RegisterFilterEventMapper(AggregateType, NotificationPolicyChangedEventType, NotificationPolicyChangedEventMapper). - RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper) + RegisterFilterEventMapper(AggregateType, NotificationPolicyRemovedEventType, NotificationPolicyRemovedEventMapper). + RegisterFilterEventMapper(AggregateType, deviceauth.AddedEventType, eventstore.GenericEventMapper[deviceauth.AddedEvent]). + RegisterFilterEventMapper(AggregateType, deviceauth.ApprovedEventType, eventstore.GenericEventMapper[deviceauth.ApprovedEvent]). + RegisterFilterEventMapper(AggregateType, deviceauth.CanceledEventType, eventstore.GenericEventMapper[deviceauth.CanceledEvent]). + RegisterFilterEventMapper(AggregateType, deviceauth.RemovedEventType, eventstore.GenericEventMapper[deviceauth.RemovedEvent]) } diff --git a/internal/repository/user/human_email.go b/internal/repository/user/human_email.go index 5732a7cac0..a6a5b8a18e 100644 --- a/internal/repository/user/human_email.go +++ b/internal/repository/user/human_email.go @@ -19,6 +19,7 @@ const ( HumanEmailVerificationFailedType = emailEventPrefix + "verification.failed" HumanEmailCodeAddedType = emailEventPrefix + "code.added" HumanEmailCodeSentType = emailEventPrefix + "code.sent" + HumanEmailConfirmURLAddedType = emailEventPrefix + "confirm_url.added" ) type HumanEmailChangedEvent struct { @@ -121,8 +122,10 @@ func HumanEmailVerificationFailedEventMapper(event *repository.Event) (eventstor type HumanEmailCodeAddedEvent struct { eventstore.BaseEvent `json:"-"` - Code *crypto.CryptoValue `json:"code,omitempty"` - Expiry time.Duration `json:"expiry,omitempty"` + Code *crypto.CryptoValue `json:"code,omitempty"` + Expiry time.Duration `json:"expiry,omitempty"` + URLTemplate string `json:"url_template,omitempty"` + CodeReturned bool `json:"code_returned,omitempty"` } func (e *HumanEmailCodeAddedEvent) Data() interface{} { @@ -137,15 +140,29 @@ func NewHumanEmailCodeAddedEvent( ctx context.Context, aggregate *eventstore.Aggregate, code *crypto.CryptoValue, - expiry time.Duration) *HumanEmailCodeAddedEvent { + expiry time.Duration, +) *HumanEmailCodeAddedEvent { + return NewHumanEmailCodeAddedEventV2(ctx, aggregate, code, expiry, "", false) +} + +func NewHumanEmailCodeAddedEventV2( + ctx context.Context, + aggregate *eventstore.Aggregate, + code *crypto.CryptoValue, + expiry time.Duration, + urlTemplate string, + codeReturned bool, +) *HumanEmailCodeAddedEvent { return &HumanEmailCodeAddedEvent{ BaseEvent: *eventstore.NewBaseEventForPush( ctx, aggregate, HumanEmailCodeAddedType, ), - Code: code, - Expiry: expiry, + Code: code, + Expiry: expiry, + URLTemplate: urlTemplate, + CodeReturned: codeReturned, } } diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index ebd123f1c4..9cf6f96f40 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: Email wurde nicht geändert Empty: Email ist leer IDMissing: Email ID fehlt + InvalidURLTemplate: URL Template ist ungültig Phone: NotFound: Telefonnummer nicht gefunden Invalid: Telefonnummer ist ungültig diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 6049ea0ed3..c5d3c36b2c 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: Email not changed Empty: Email is empty IDMissing: Email ID is missing + InvalidURLTemplate: URL Template is invalid Phone: NotFound: Phone not found Invalid: Phone is invalid diff --git a/internal/static/i18n/es.yaml b/internal/static/i18n/es.yaml index 7ac109b3d6..63ffb3981c 100644 --- a/internal/static/i18n/es.yaml +++ b/internal/static/i18n/es.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: El email no ha cambiado Empty: El email no está vacío IDMissing: Falta el ID del email + InvalidURLTemplate: La plantilla URL no es válida Phone: NotFound: Teléfono no encontrado Invalid: El teléfono no es válido diff --git a/internal/static/i18n/fr.yaml b/internal/static/i18n/fr.yaml index 77043e07a5..184cd83f06 100644 --- a/internal/static/i18n/fr.yaml +++ b/internal/static/i18n/fr.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: L'adresse électronique n'a pas changé Empty: Email est vide IDMissing: Email ID manquant + InvalidURLTemplate: Le modèle d'URL n'est pas valide Phone: Notfound: Téléphone non trouvé Invalid: Le téléphone n'est pas valide diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 426d0f4537..1f9056cf5f 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: Email non cambiata Empty: Email è vuota IDMissing: Email ID mancante + InvalidURLTemplate: Il modello di URL non è valido Phone: NotFound: Telefono non trovato Invalid: Il telefono non è valido diff --git a/internal/static/i18n/ja.yaml b/internal/static/i18n/ja.yaml index 7c07972e1b..8f347b02d1 100644 --- a/internal/static/i18n/ja.yaml +++ b/internal/static/i18n/ja.yaml @@ -76,6 +76,7 @@ Errors: Invalid: 無効なメールアドレスです AlreadyVerified: メールアドレスはすでに検証済みです NotChanged: メールアドレスが変更されていません + InvalidURLTemplate: URLテンプレートが無効です Phone: NotFound: 電話番号が見つかりません Invalid: 無効な電話番号です diff --git a/internal/static/i18n/pl.yaml b/internal/static/i18n/pl.yaml index 4f69639a7a..0ef614bff8 100644 --- a/internal/static/i18n/pl.yaml +++ b/internal/static/i18n/pl.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: Adres e-mail nie zmieniony Empty: Adres e-mail jest pusty IDMissing: Adres e-mail ID brakuje + InvalidURLTemplate: Szablon URL jest nieprawidłowy Phone: NotFound: Numer telefonu nie znaleziony Invalid: Numer telefonu jest nieprawidłowy diff --git a/internal/static/i18n/zh.yaml b/internal/static/i18n/zh.yaml index 96b731f6bf..31c679ea21 100644 --- a/internal/static/i18n/zh.yaml +++ b/internal/static/i18n/zh.yaml @@ -81,6 +81,7 @@ Errors: NotChanged: 电子邮件未更改 Empty: 电子邮件是空的 IDMissing: 电子邮件ID丢失 + InvalidURLTemplate: URL模板无效 Phone: NotFound: 手机号码未找到 Invalid: 手机号码无效 diff --git a/internal/telemetry/metrics/otel/open_telemetry.go b/internal/telemetry/metrics/otel/open_telemetry.go index 4a18c6171c..e249f00d88 100644 --- a/internal/telemetry/metrics/otel/open_telemetry.go +++ b/internal/telemetry/metrics/otel/open_telemetry.go @@ -18,7 +18,7 @@ import ( ) type Metrics struct { - Exporter *prometheus.Exporter + Provider metric.MeterProvider Meter metric.Meter Counters sync.Map UpDownSumObserver sync.Map @@ -34,12 +34,13 @@ func NewMetrics(meterName string) (metrics.Metrics, error) { if err != nil { return &Metrics{}, err } + meterProvider := sdk_metric.NewMeterProvider( + sdk_metric.WithReader(exporter), + sdk_metric.WithResource(resource), + ) return &Metrics{ - Exporter: exporter, - Meter: sdk_metric.NewMeterProvider( - sdk_metric.WithReader(exporter), - sdk_metric.WithResource(resource), - ).Meter(meterName), + Provider: meterProvider, + Meter: meterProvider.Meter(meterName), }, nil } @@ -48,7 +49,7 @@ func (m *Metrics) GetExporter() http.Handler { } func (m *Metrics) GetMetricsProvider() metric.MeterProvider { - return sdk_metric.NewMeterProvider(sdk_metric.WithReader(m.Exporter)) + return m.Provider } func (m *Metrics) RegisterCounter(name, description string) error { diff --git a/pkg/grpc/user/v2alpha/user.go b/pkg/grpc/user/v2alpha/user.go new file mode 100644 index 0000000000..f419594dfb --- /dev/null +++ b/pkg/grpc/user/v2alpha/user.go @@ -0,0 +1,5 @@ +package user + +func (r *AddHumanUserRequest) AuthContext() string { + return r.GetOrganisation().GetOrgId() +} diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index da7ccb4e48..1424d7cd29 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -2801,7 +2801,7 @@ service AdminService { rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) { option (google.api.http) = { - delete: "/text/message/verifyemail/{language}" + delete: "/text/message/passwordreset/{language}" }; option (zitadel.v1.auth_option) = { diff --git a/proto/zitadel/app.proto b/proto/zitadel/app.proto index d889135bc4..dcd5da5c25 100644 --- a/proto/zitadel/app.proto +++ b/proto/zitadel/app.proto @@ -180,6 +180,7 @@ enum OIDCGrantType{ OIDC_GRANT_TYPE_AUTHORIZATION_CODE = 0; OIDC_GRANT_TYPE_IMPLICIT = 1; OIDC_GRANT_TYPE_REFRESH_TOKEN = 2; + OIDC_GRANT_TYPE_DEVICE_CODE = 3; } enum OIDCAppType { diff --git a/proto/zitadel/management.proto b/proto/zitadel/management.proto index 1c92cffd83..cf055e4a55 100644 --- a/proto/zitadel/management.proto +++ b/proto/zitadel/management.proto @@ -5588,7 +5588,7 @@ service ManagementService { rpc ResetCustomPasswordResetMessageTextToDefault(ResetCustomPasswordResetMessageTextToDefaultRequest) returns (ResetCustomPasswordResetMessageTextToDefaultResponse) { option (google.api.http) = { - delete: "/text/message/verifyemail/{language}" + delete: "/text/message/passwordreset/{language}" }; option (zitadel.v1.auth_option) = { diff --git a/proto/zitadel/object/v2alpha/object.proto b/proto/zitadel/object/v2alpha/object.proto new file mode 100644 index 0000000000..3a209b6371 --- /dev/null +++ b/proto/zitadel/object/v2alpha/object.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package zitadel.object.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha;object"; + +import "google/protobuf/timestamp.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message Organisation { + oneof org { + string org_id = 1; + } +} + +message Details { + //sequence represents the order of events. It's always counting + // + // on read: the sequence of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + uint64 sequence = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"2\""; + } + ]; + //change_date is the timestamp when the object was changed + // + // on read: the timestamp of the last event reduced by the projection + // + // on manipulation: the timestamp of the event(s) added by the manipulation + google.protobuf.Timestamp change_date = 2; + //resource_owner is the organization or instance_id an object belongs to + string resource_owner = 3 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"69629023906488334\""; + } + ]; +} diff --git a/proto/zitadel/user/v2alpha/email.proto b/proto/zitadel/user/v2alpha/email.proto new file mode 100644 index 0000000000..151348b55a --- /dev/null +++ b/proto/zitadel/user/v2alpha/email.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package zitadel.user.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message SetHumanEmail { + string email = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"mini@mouse.com\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 2; + ReturnEmailVerificationCode return_code = 3; + bool is_verified = 4 [(validate.rules).bool.const = true]; + } +} + +message SendEmailVerificationCode { + optional string url_template = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}\""; + description: "\"Optionally set a url_template, which will be used in the verification mail sent by ZITADEL to guide the user to your verification page. If no template is set, the default ZITADEL url will be used.\"" + } + ]; +} + +message ReturnEmailVerificationCode {} + diff --git a/proto/zitadel/user/v2alpha/password.proto b/proto/zitadel/user/v2alpha/password.proto new file mode 100644 index 0000000000..eb168f0436 --- /dev/null +++ b/proto/zitadel/user/v2alpha/password.proto @@ -0,0 +1,53 @@ +syntax = "proto3"; + +package zitadel.user.v2alpha; + +option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; + +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + +message SetUserPassword { + oneof type { + Password password = 1; + HashedPassword hashed_password = 2; + } +} + +message Password { + string password = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"Secr3tP4ssw0rd!\""; + min_length: 1, + max_length: 200; + } + ]; + bool change_required = 2; +} + +message HashedPassword { + string hash = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"$2a$12$lJ08fqVr8bFJilRVnDT9QeULI7YW.nT3iwUv6dyg0aCrfm3UY8XR2\""; + description: "\"hashed password\""; + min_length: 1, + max_length: 200; + } + ]; + string algorithm = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, const: "bcrypt"}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"bcrypt\""; + description: "\"algorithm used for the hash. currently only bcrypt is supported\""; + min_length: 1, + max_length: 200; + } + ]; + bool change_required = 3; +} diff --git a/proto/zitadel/user/v2alpha/user.proto b/proto/zitadel/user/v2alpha/user.proto index 9b8426f517..d6312eb3e1 100644 --- a/proto/zitadel/user/v2alpha/user.proto +++ b/proto/zitadel/user/v2alpha/user.proto @@ -4,6 +4,87 @@ package zitadel.user.v2alpha; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; +import "validate/validate.proto"; + message User { string id = 1; } + +enum Gender { + GENDER_UNSPECIFIED = 0; + GENDER_FEMALE = 1; + GENDER_MALE = 2; + GENDER_DIVERSE = 3; +} + +message SetHumanProfile { + string first_name = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Minnie\""; + } + ]; + string last_name = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"Mouse\""; + } + ]; + optional string nick_name = 3 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Mini\""; + } + ]; + optional string display_name = 4 [ + (validate.rules).string = {max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 200; + example: "\"Minnie Mouse\""; + } + ]; + optional string preferred_language = 5 [ + (validate.rules).string = {max_len: 10}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + max_length: 10; + example: "\"en\""; + } + ]; + optional zitadel.user.v2alpha.Gender gender = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"GENDER_FEMALE\""; + } + ]; +} + + +message SetMetadataEntry { + string key = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"my-key\""; + min_length: 1, + max_length: 200; + } + ]; + bytes value = 2 [ + (validate.rules).bytes = {min_len: 1, max_len: 500000}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The value has to be base64 encoded."; + example: "\"VGhpcyBpcyBteSB0ZXN0IHZhbHVl\""; + min_length: 1, + max_length: 500000; + } + ]; +} diff --git a/proto/zitadel/user/v2alpha/user_service.proto b/proto/zitadel/user/v2alpha/user_service.proto index f16bb8ecfa..2a4629779e 100644 --- a/proto/zitadel/user/v2alpha/user_service.proto +++ b/proto/zitadel/user/v2alpha/user_service.proto @@ -3,76 +3,244 @@ syntax = "proto3"; package zitadel.user.v2alpha; import "zitadel/options.proto"; +import "zitadel/object/v2alpha/object.proto"; +import "zitadel/user/v2alpha/email.proto"; +import "zitadel/user/v2alpha/password.proto"; import "zitadel/user/v2alpha/user.proto"; import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "protoc-gen-openapiv2/options/annotations.proto"; import "validate/validate.proto"; option go_package = "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha;user"; +option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { + info: { + title: "User Service"; + version: "2.0-alpha"; + description: "This API is intended to manage users in a ZITADEL instance. This project is in alpha state. It can AND will continue breaking until the services provide the same functionality as the current login."; + contact:{ + name: "ZITADEL" + url: "https://zitadel.com" + email: "hi@zitadel.com" + } + license: { + name: "Apache 2.0", + url: "https://github.com/zitadel/zitadel/blob/main/LICENSE"; + }; + }; + schemes: HTTPS; + schemes: HTTP; + + consumes: "application/json"; + consumes: "application/grpc"; + + produces: "application/json"; + produces: "application/grpc"; + + consumes: "application/grpc-web+proto"; + produces: "application/grpc-web+proto"; + + host: "$ZITADEL_DOMAIN"; + base_path: "/"; + + external_docs: { + description: "Detailed information about ZITADEL", + url: "https://zitadel.com/docs" + } + + responses: { + key: "403"; + value: { + description: "Returned when the user does not have permission to access the resource."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } + responses: { + key: "404"; + value: { + description: "Returned when the resource does not exist."; + schema: { + json_schema: { + ref: "#/definitions/rpcStatus"; + } + } + } + } +}; + service UserService { - // TestGet simply demonstrates how the context (org, instance) could be handled in a GET request // - // - // this request is subject to change and currently used for demonstration only - rpc TestGet (TestGetRequest) returns (TestGetResponse) { + // Create a new human user + rpc AddHumanUser (AddHumanUserRequest) returns (AddHumanUserResponse) { option (google.api.http) = { - get: "/v2alpha/users/test" - }; - } - - // TestPOST simply demonstrates how the context (org, instance) could be handled in a POST request - // - // this request is subject to change and currently used for demonstration only - rpc TestPost (TestPostRequest) returns (TestPostResponse) { - option (google.api.http) = { - post: "/v2alpha/users/test" + post: "/v2alpha/users/human" body: "*" }; + + option (zitadel.v1.auth_option) = { + permission: "user.write" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Create a user (Human)"; + description: "Create/import a new user with the type human. The newly created user will get a verification email if either the email address is not marked as verified and you did not request the verification to be returned." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; } - // TestAuth demonstrates how the context (org, instance) could be handled in combination of the authorized context - // - // this request is subject to change and currently used for demonstration only - rpc TestAuth (TestAuthRequest) returns (TestAuthResponse) { + // Change the email of a user + rpc SetEmail (SetEmailRequest) returns (SetEmailResponse) { option (google.api.http) = { - get: "/v2alpha/users/test_auth" + post: "/v2alpha/users/{user_id}/email" + body: "*" }; option (zitadel.v1.auth_option) = { permission: "authenticated" }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Change the user email"; + description: "Change the email address of a user. If the state is set to not verified, a verification code will be generated, which can be either returned or sent to the user by email." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; + } + + // Verify the email with the provided code + rpc VerifyEmail (VerifyEmailRequest) returns (VerifyEmailResponse) { + option (google.api.http) = { + post: "/v2alpha/users/{user_id}/email/_verify" + body: "*" + }; + + option (zitadel.v1.auth_option) = { + permission: "authenticated" + }; + + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Verify the email"; + description: "Verify the email with the generated code." + responses: { + key: "200" + value: { + description: "OK"; + } + }; + }; } } -message TestGetRequest{ - Context ctx = 1; -} - -message TestGetResponse{ - string ctx = 1; -} - -message TestPostRequest{ - Context ctx = 1; -} - -message TestPostResponse{ - string ctx = 1; -} - -message TestAuthRequest{ - Context ctx = 1; -} - -message TestAuthResponse{ - User user = 1; - Context ctx = 2; -} - -message Context { - oneof ctx { - bool instance = 1; - string org_id = 2; - string org_domain = 3; +message AddHumanUserRequest{ + // optionally set your own id unique for the user + optional string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"d654e6ba-70a3-48ef-a95d-37c8d8a7901a\""; + } + ]; + // optionally set a unique username, if none is provided the email will be used + optional string username = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"minnie-mouse\""; + } + ]; + zitadel.object.v2alpha.Organisation organisation = 3; + SetHumanProfile profile = 4 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + SetHumanEmail email = 5 [ + (validate.rules).message.required = true, + (google.api.field_behavior) = REQUIRED + ]; + repeated SetMetadataEntry metadata = 6; + oneof password_type { + Password password = 7; + HashedPassword hashed_password = 8; } } + +message AddHumanUserResponse { + string user_id = 1; + zitadel.object.v2alpha.Details details = 2; + optional string email_code = 3; +} + +message SetEmailRequest{ + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + string email = 2 [ + (validate.rules).string = {min_len: 1, max_len: 200, email: true}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"mini@mouse.com\""; + } + ]; + // if no verification is specified, an email is sent with the default url + oneof verification { + SendEmailVerificationCode send_code = 3; + ReturnEmailVerificationCode return_code = 4; + bool is_verified = 5 [(validate.rules).bool.const = true]; + } +} + +message SetEmailResponse{ + zitadel.object.v2alpha.Details details = 1; + // in case the verification was set to return_code, the code will be returned + optional string verification_code = 2; +} + +message VerifyEmailRequest{ + string user_id = 1 [ + (validate.rules).string = {min_len: 1, max_len: 200}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 200; + example: "\"69629026806489455\""; + } + ]; + string verification_code = 2 [ + (validate.rules).string = {min_len: 1, max_len: 20}, + (google.api.field_behavior) = REQUIRED, + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + min_length: 1; + max_length: 20; + example: "\"SKJd342k\""; + description: "\"the verification code generated during the set email request\""; + } + ]; +} + +message VerifyEmailResponse{ + zitadel.object.v2alpha.Details details = 1; +} diff --git a/tools/go.mod b/tools/go.mod index 604ee5704d..b2135488f2 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -3,17 +3,28 @@ module github.com/zitadel/zitadel/tools go 1.15 require ( - github.com/envoyproxy/protoc-gen-validate v0.6.1 + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect + github.com/Masterminds/sprig v2.22.0+incompatible // indirect + github.com/envoyproxy/protoc-gen-validate v0.10.1 github.com/go-bindata/go-bindata/v3 v3.1.3 - github.com/golang/mock v1.4.4 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0 - github.com/iancoleman/strcase v0.1.3 // indirect - github.com/kisielk/errcheck v1.5.0 // indirect - github.com/lyft/protoc-gen-star v0.5.2 // indirect - github.com/pseudomuto/protoc-gen-doc v1.4.1 + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.1.1 // indirect + github.com/golang/mock v1.6.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 + github.com/huandu/xstrings v1.4.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect + github.com/kisielk/errcheck v1.6.3 // indirect + github.com/lyft/protoc-gen-star/v2 v2.0.3 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mwitkow/go-proto-validators v0.3.2 // indirect + github.com/pseudomuto/protoc-gen-doc v1.5.1 + github.com/pseudomuto/protokit v0.2.1 // indirect github.com/rakyll/statik v0.1.7 - github.com/spf13/afero v1.5.1 // indirect - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 - google.golang.org/protobuf v1.26.0 - gopkg.in/yaml.v2 v2.4.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/tools v0.8.0 // indirect + google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 + google.golang.org/protobuf v1.30.0 ) diff --git a/tools/go.sum b/tools/go.sum index e087e3f397..98976a176f 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -3,6 +3,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -13,62 +14,681 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.15.0+incompatible h1:0gSxPGWS9PAr7U2NsQ2YQg6juRDINkUyuvbb4b2Xm8w= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.15.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= +github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aokoli/goutils v1.0.1 h1:7fpzNGoJ3VA8qcrm++XEE1QUe0mIwNeLa02Nwq7RDkg= github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g9DP+DQ= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0-java/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.1 h1:4CF52PCseTFt4bE+Yk3dIpdVi7XWuPVMhPtm4FaIJPM= -github.com/envoyproxy/protoc-gen-validate v0.6.1/go.mod h1:txg5va2Qkip90uYoSKH+nkAAmXrb2j3iq4FLwdrCbXQ= -github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-bindata/go-bindata/v3 v3.1.3 h1:F0nVttLC3ws0ojc7p60veTurcOm//D4QBODNM7EGrCI= github.com/go-bindata/go-bindata/v3 v3.1.3/go.mod h1:1/zrpXsLD8YDIbhZRqXzm1Ghc7NhEvIN9+Z6R5/xH4I= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -78,8 +698,11 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -95,10 +718,15 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 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= @@ -106,11 +734,20 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -118,93 +755,201 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0 h1:HlJcTiqGHvaWDG7/s85d68Kw7G7FqMz+9LlcyVauOAw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.2.0/go.mod h1:gRq9gZWcIFvz68EgWqy2qQpRbmtn5j2qLZ4zHjqiLpg= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/huandu/xstrings v1.0.0 h1:pO2K/gKgKaat5LdpAhxhluX2GPQMaI3W5FUz/I/UnWk= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= -github.com/iancoleman/strcase v0.0.0-20180726023541-3605ed457bf7/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= -github.com/iancoleman/strcase v0.1.3 h1:dJBk1m2/qjL1twPLf68JND55vvivMupZ4wIzE8CTdBw= -github.com/iancoleman/strcase v0.1.3/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.4 h1:mKkfHkZWD8dC7WxKx3N9WCF0Y+dLau45704YQmY6H94= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.4/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/errcheck v1.6.3 h1:dEKh+GLHcWm2oN34nMvDzn1sqI0i0WxPvrgiJA5JuM8= +github.com/kisielk/errcheck v1.6.3/go.mod h1:nXw/i/MfnvRHqXa7XXmQMUB0oNFGuBrNI8d8NLy0LPw= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -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/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -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/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= -github.com/lyft/protoc-gen-star v0.5.2 h1:ICQPpOr4uO46eme1Y5Jj0fnJkc9/upQ9xxt0+2AmUDQ= -github.com/lyft/protoc-gen-star v0.5.2/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= -github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007 h1:28i1IjGcx8AofiB4N3q5Yls55VEaitzuEPkFJEVgGkA= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1 h1:erE0rdztuaDq3bpGifD95wfoPrSZc95nGA6tbiNYh6M= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/lyft/protoc-gen-star/v2 v2.0.3 h1:/3+/2sWyXeMLzKd1bX+ixWKgEMsULrIivpDsuaF441o= +github.com/lyft/protoc-gen-star/v2 v2.0.3/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod h1:m2XC9Qq0AlmmVksL6FktJCdTYyLk7V3fKyp0sl1yWQo= +github.com/mwitkow/go-proto-validators v0.3.2 h1:qRlmpTzm2pstMKKzTdvwPCF5QfBNURSlAgN/R+qbKos= +github.com/mwitkow/go-proto-validators v0.3.2/go.mod h1:ej0Qp0qMgHN/KtDyUt+Q1/tA7a5VarXUOUxD+oeD30w= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/pseudomuto/protoc-gen-doc v1.4.1 h1:aNTZq0dy0Pq2ag2v7bhNKFNgBBA8wMCoJSChhd7RciE= -github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= -github.com/pseudomuto/protokit v0.2.0 h1:hlnBDcy3YEDXH7kc9gV+NLaN0cDzhDvD1s7Y6FZ8RpM= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pseudomuto/protoc-gen-doc v1.5.1 h1:Ah259kcrio7Ix1Rhb6u8FCaOkzf9qRBqXnvAufg061w= +github.com/pseudomuto/protoc-gen-doc v1.5.1/go.mod h1:XpMKYg6zkcpgfpCfQ8GcWBDRtRxOmMR5w7pz4Xo+dYM= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= +github.com/pseudomuto/protokit v0.2.1 h1:kCYpE3thoR6Esm0CUvd5xbrDTOZPvQPTDeyXpZfrJdk= +github.com/pseudomuto/protokit v0.2.1/go.mod h1:gt7N5Rz2flBzYafvaxyIxMZC0TTF5jDZfRnw25hAAyo= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.5.1 h1:VHu76Lk0LSP1x254maIu2bplkWpfBWI+B+6fdoZprcg= -github.com/spf13/afero v1.5.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v0.0.0-20170130113145-4d4bfba8f1d1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -214,8 +959,10 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -223,8 +970,19 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -250,23 +1008,84 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -280,6 +1099,7 @@ golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -291,20 +1111,94 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -317,6 +1211,7 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -339,18 +1234,49 @@ golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d h1:W07d4xkoAUSNOkOzdzXCdFGxT7o2rW4q8M34tB2i//k= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= +golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -367,14 +1293,57 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180427144745-86e600f69ee4/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -397,14 +1366,117 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea h1:N98SvVh7Hdle2lgUVFuIkf0B3u29CUakMUQa7Hwz8Wc= -google.golang.org/genproto v0.0.0-20210207032614-bba0dbe2a9ea/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -417,11 +1489,37 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0 h1:rNBFJjBCOgVr9pWD7rs/knKL4FRTKgpZmsRfV214zcA= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.3.0/go.mod h1:Dk1tviKTvMCz5tvh7t+fh94dhmQVHuCt2OzJB3CTW9Y= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -433,18 +1531,24 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -452,6 +1556,42 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=