chore: merge main (#5776)
@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
branches: [
|
||||
{name: 'main'},
|
||||
{name: 'next'}
|
||||
{name: 'next'},
|
||||
],
|
||||
plugins: [
|
||||
"@semantic-release/commit-analyzer"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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:
|
||||
|
@ -76,6 +76,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
@ -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
|
||||
}
|
||||
|
6
cmd/setup/10_create_temp_table.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE temporary TABLE IF NOT EXISTS wrong_events (
|
||||
instance_id TEXT
|
||||
, event_sequence BIGINT
|
||||
, current_cd TIMESTAMPTZ
|
||||
, next_cd TIMESTAMPTZ
|
||||
);
|
@ -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;
|
||||
);
|
1
cmd/setup/10_update.sql
Normal file
@ -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;
|
51
cmd/setup/cleanup.go
Normal file
@ -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")
|
||||
}
|
@ -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 {
|
||||
|
@ -45,6 +45,8 @@ Requirements:
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(NewCleanup())
|
||||
|
||||
Flags(cmd)
|
||||
|
||||
return cmd
|
||||
|
@ -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()
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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*
|
||||
<ul><li>0: unspecified</li><li>1: active</li><li>2: inactive</li><li>3: removed</li></ul>
|
||||
- `creationDate` *Date*
|
||||
|
@ -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.
|
@ -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.
|
||||
|
||||
<GeneralConfigDescription provider_account="Microsoft account" />
|
||||
|
||||
![Azure Provider](/img/guides/zitadel_azure_provider.png)
|
||||
![Azure Provider](/img/guides/zitadel_azure_provider2.png)
|
||||
|
||||
### Activate IdP
|
||||
|
||||
|
@ -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.
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
title: Connect with Auth0 through OIDC
|
||||
sidebar_label: Auth0 (OIDC)
|
||||
---
|
||||
|
||||
import CreateApp from "../application/_application.mdx";
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
132
docs/docs/guides/integrate/services/google-cloud.mdx
Normal file
@ -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.
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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. <!-- TODO: How to enable it for SaaS ZITADEL? -->
|
||||
- [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
|
||||
|
@ -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=<organisation id> --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.
|
||||
|
@ -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.
|
||||
|
127
docs/docs/guides/solution-scenarios/domain-discovery.mdx
Normal file
@ -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
|
@ -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.
|
||||
|
||||
<ListWrapper title="Solution Scenarios">
|
||||
<ListElement
|
||||
link="./b2c"
|
||||
iconClasses="las la-paragraph"
|
||||
roundClasses="custom-rounded rounded-split"
|
||||
label="B2C"
|
||||
title="Business to Consumer"
|
||||
description="Organizations with your SDLC, Domains, Authentication, Hosted Login"
|
||||
/>
|
||||
<ListElement
|
||||
link="./b2b"
|
||||
iconClasses="las la-paragraph"
|
||||
roundClasses="custom-rounded rounded-split"
|
||||
label="B2B"
|
||||
title="Business to Business"
|
||||
description="Planning considerations, B2B Sample Case"
|
||||
/>
|
||||
</ListWrapper>
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -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: [
|
||||
|
@ -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);
|
||||
|
BIN
docs/static/img/guides/integrate/services/google-cloud-action-code.png
vendored
Normal file
After Width: | Height: | Size: 189 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-action-flow.png
vendored
Normal file
After Width: | Height: | Size: 165 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-create-app.png
vendored
Normal file
After Width: | Height: | Size: 219 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-redirect-url.png
vendored
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
docs/static/img/guides/integrate/services/google-cloud-token-settings.png
vendored
Normal file
After Width: | Height: | Size: 217 KiB |
BIN
docs/static/img/guides/solution-scenarios/domain-discovery.png
vendored
Normal file
After Width: | Height: | Size: 321 KiB |
BIN
docs/static/img/guides/zitadel_azure_provider.png
vendored
Before Width: | Height: | Size: 109 KiB |
BIN
docs/static/img/guides/zitadel_azure_provider2.png
vendored
Normal file
After Width: | Height: | Size: 366 KiB |
10
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
|
||||
|
16
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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
19
internal/api/grpc/object/v2/converter.go
Normal file
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
@ -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 {
|
||||
|
65
internal/api/grpc/user/v2/email.go
Normal file
@ -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
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
112
internal/api/grpc/user/v2/user.go
Normal file
@ -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
|
||||
}
|
80
internal/api/grpc/user/v2/user_test.go
Normal file
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
176
internal/api/oidc/device_auth.go
Normal file
@ -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.
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
201
internal/api/ui/login/device_auth.go
Normal file
@ -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)
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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: "(オプション)"
|
||||
|
@ -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)
|
||||
|
@ -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: (可选)
|
||||
|
18
internal/api/ui/login/static/templates/device_action.html
Normal file
@ -0,0 +1,18 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<h1>{{.Title}}</h1>
|
||||
<p>
|
||||
{{.Username}}, {{t "DeviceAuth.Action.GrantDevice"}} {{.ClientID}} {{t "DeviceAuth.Action.AccessToScopes"}}: {{.Scopes}}.
|
||||
</p>
|
||||
<form method="POST">
|
||||
{{ .CSRF }}
|
||||
<input type="hidden" name="authRequestID" value="{{.AuthRequestID}}">
|
||||
<button class="lgn-raised-button lgn-primary left" type="submit" formaction="./allowed">
|
||||
{{t "DeviceAuth.Action.Button.Allow"}}
|
||||
</button>
|
||||
<button class="lgn-raised-button lgn-warn right" type="submit" formaction="./denied">
|
||||
{{t "DeviceAuth.Action.Button.Deny"}}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{{template "main-bottom" .}}
|
21
internal/api/ui/login/static/templates/device_usercode.html
Normal file
@ -0,0 +1,21 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<h1>{{.Title}}</h1>
|
||||
<form method="POST">
|
||||
|
||||
{{ .CSRF }}
|
||||
|
||||
<div class="fields">
|
||||
<label class="lgn-label" for="user_code">{{t "DeviceAuth.UserCode.Label"}}</label>
|
||||
<input class="lgn-input" id="user_code" name="user_code" autofocus required{{if .ErrMessage}} shake{{end}}>
|
||||
</div>
|
||||
|
||||
{{template "error-message" .}}
|
||||
|
||||
<div class="lgn-actions">
|
||||
<span class="fill-space"></span>
|
||||
<button id="submit-button" class="lgn-raised-button lgn-primary right" type="submit">{{t "DeviceAuth.UserCode.ButtonNext"}}</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{{template "main-bottom" .}}
|
12
internal/api/ui/login/static/templates/success.html
Normal file
@ -0,0 +1,12 @@
|
||||
{{template "main-top" .}}
|
||||
|
||||
<div class="lgn-head">
|
||||
<div class="lgn-actions">
|
||||
<i class="lgn-icon-check-solid lgn-primary"></i>
|
||||
<p class="lgn-error-message">
|
||||
{{ .Message }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{template "main-bottom" .}}
|
@ -1,3 +1,3 @@
|
||||
package statik
|
||||
|
||||
//go:generate statik -src=../static -dest=.. -ns=login
|
||||
//go:generate statik -f -src=../static -dest=.. -ns=login
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|