chore: merge main (#5776)

This commit is contained in:
Silvan 2023-04-28 17:34:32 +02:00 committed by GitHub
commit a506b5c54f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
208 changed files with 8047 additions and 970 deletions

View File

@ -1,7 +1,7 @@
module.exports = {
branches: [
{name: 'main'},
{name: 'next'}
{name: 'next'},
],
plugins: [
"@semantic-release/commit-analyzer"

View File

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

View File

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

View File

@ -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"

View File

@ -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:

View File

@ -76,6 +76,7 @@ func (mig *FirstInstance) Execute(ctx context.Context) error {
nil,
nil,
nil,
nil,
)
if err != nil {

View File

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

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

View File

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

View File

@ -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 {

View File

@ -45,6 +45,8 @@ Requirements:
},
}
cmd.AddCommand(NewCleanup())
Flags(cmd)
return cmd

View File

@ -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()

View File

@ -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"

View File

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

View File

@ -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*

View File

@ -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.

View File

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

View File

@ -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.

View File

@ -1,5 +1,6 @@
---
title: Connect with Auth0 through OIDC
sidebar_label: Auth0 (OIDC)
---
import CreateApp from "../application/_application.mdx";

View File

@ -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.

View File

@ -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.

View File

@ -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.

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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

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

View File

@ -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.

View File

@ -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.

View 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

View File

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

View File

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

View File

@ -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",
},
}
}
},

View File

@ -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: [

View File

@ -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);

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

10
go.mod
View File

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

@ -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=

View File

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

View File

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

View File

@ -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) {

View File

@ -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 {

View File

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

View File

@ -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) {

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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) {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

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

View File

@ -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,
}
}

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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 {

View File

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

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

View File

@ -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")
}

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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: "(オプション)"

View File

@ -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)

View File

@ -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: (可选)

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

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

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

View File

@ -1,3 +1,3 @@
package statik
//go:generate statik -src=../static -dest=.. -ns=login
//go:generate statik -f -src=../static -dest=.. -ns=login

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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)

Some files were not shown because too many files have changed in this diff Show More